diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md
index aa121469..876b5aee 100644
--- a/docs/releases/unreleased.md
+++ b/docs/releases/unreleased.md
@@ -23,3 +23,7 @@ Example:
```
-->
+
+## Fixed
+
+- Google Calendar task descriptions now use mobile-friendly plain text for Obsidian links and display labels for wiki-style project/context links.
diff --git a/src/services/TaskCalendarSyncService.ts b/src/services/TaskCalendarSyncService.ts
index 01af8cc3..3a32813e 100644
--- a/src/services/TaskCalendarSyncService.ts
+++ b/src/services/TaskCalendarSyncService.ts
@@ -266,12 +266,12 @@ export class TaskCalendarSyncService {
// Add contexts
if (task.contexts && task.contexts.length > 0) {
- parts.push(t("contexts", { value: task.contexts.map((c) => `@${c}`).join(", ") }));
+ parts.push(t("contexts", { value: task.contexts.map((c) => `@${this.toCalendarDescriptionLabel(c)}`).join(", ") }));
}
// Add projects
if (task.projects && task.projects.length > 0) {
- parts.push(t("projects", { value: task.projects.join(", ") }));
+ parts.push(t("projects", { value: task.projects.map((p) => this.toCalendarDescriptionLabel(p)).join(", ") }));
}
// Add separator before link
@@ -285,14 +285,28 @@ export class TaskCalendarSyncService {
const vaultName = this.plugin.app.vault.getName();
const encodedPath = encodeURIComponent(task.path);
const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedPath}`;
- // Google Calendar renders HTML in descriptions, so use an anchor tag
const linkText = t("openInObsidian");
- parts.push(`${linkText}`);
+ parts.push(`${linkText}: ${obsidianUri}`);
}
return parts.join("\n");
}
+ private toCalendarDescriptionLabel(value: string): string {
+ return value
+ .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2")
+ .replace(/\[\[([^\]]+)\]\]/g, (_match, target: string) => this.basenameForDisplay(target))
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1")
+ .trim();
+ }
+
+ private basenameForDisplay(target: string): string {
+ const withoutHeading = target.split("#")[0];
+ const withoutExtension = withoutHeading.replace(/\.md$/i, "");
+ const basename = withoutExtension.split("/").pop();
+ return basename || withoutExtension || target;
+ }
+
/**
* Get the date to use for the calendar event based on settings
*/
diff --git a/tests/services/TaskCalendarSyncService.test.ts b/tests/services/TaskCalendarSyncService.test.ts
index 21c0dd4b..dd20660d 100644
--- a/tests/services/TaskCalendarSyncService.test.ts
+++ b/tests/services/TaskCalendarSyncService.test.ts
@@ -14,19 +14,38 @@ describe("TaskCalendarSyncService", () => {
googleCalendarExport: {
syncOnTaskUpdate: true,
targetCalendarId: "test-calendar",
+ includeObsidianLink: true,
}
},
+ app: {
+ vault: {
+ getName: jest.fn().mockReturnValue("Martin OS"),
+ },
+ },
cacheManager: {
getTaskInfo: jest.fn()
},
statusManager: {
- getStatusConfig: jest.fn().mockReturnValue({ label: "Todo" })
+ getStatusConfig: jest.fn((status: string) => ({ label: status === "ready" ? "Ready" : "Todo" }))
},
priorityManager: {
- getPriorityConfig: jest.fn().mockReturnValue({ label: "High" })
+ getPriorityConfig: jest.fn((priority: string) => ({ label: priority === "2-high" ? "High" : "Medium" }))
},
i18n: {
- translate: jest.fn().mockReturnValue("Untitled Task")
+ translate: jest.fn((key: string, params?: Record) => {
+ const translations: Record = {
+ "settings.integrations.googleCalendarExport.eventDescription.untitledTask": "Untitled Task",
+ "settings.integrations.googleCalendarExport.eventDescription.priority": "Priority: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.status": "Status: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.scheduled": "Scheduled: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.timeEstimate": "Time Estimate: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.contexts": "Contexts: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.projects": "Projects: {value}",
+ "settings.integrations.googleCalendarExport.eventDescription.openInObsidian": "Open in Obsidian",
+ };
+ const translation = translations[key] || key;
+ return translation.replace(/\{(\w+)\}/g, (_match, name) => String(params?.[name] ?? ""));
+ })
}
};
@@ -78,4 +97,38 @@ describe("TaskCalendarSyncService", () => {
expect(syncService.executeTaskUpdate).toHaveBeenCalledTimes(1);
expect(syncService.executeTaskUpdate).toHaveBeenCalledWith(secondPayload);
});
+
+ it("should build plain-text calendar descriptions for external calendar clients", () => {
+ const description = syncService.buildEventDescription({
+ path: "1 Tasks/Tasks/Download first personal data export batch.md",
+ title: "Download first personal data export batch",
+ status: "ready",
+ priority: "2-high",
+ scheduled: "2026-04-29",
+ timeEstimate: 180,
+ projects: [
+ "[[0 Collect personal data exports for vault intelligence|Collect personal data exports for vault intelligence]]",
+ "[[Projects/Nested Project.md]]",
+ "[Markdown Project](Projects/Markdown%20Project.md)",
+ ],
+ contexts: ["[[People/Martin Ball|Martin Ball]]", "admin"],
+ } as TaskInfo);
+
+ expect(description).toContain("Priority: High");
+ expect(description).toContain("Status: Ready");
+ expect(description).toContain("Scheduled: 2026-04-29");
+ expect(description).toContain("Time Estimate: 3h 0m");
+ expect(description).toContain("Contexts: @Martin Ball, @admin");
+ expect(description).toContain(
+ "Projects: Collect personal data exports for vault intelligence, Nested Project, Markdown Project"
+ );
+ expect(description).toContain(
+ "Open in Obsidian: obsidian://open?vault=Martin%20OS&file=1%20Tasks%2FTasks%2FDownload%20first%20personal%20data%20export%20batch.md"
+ );
+ expect(description).not.toContain("[[");
+ expect(description).not.toContain("]]");
+ expect(description).not.toContain("");
+ expect(description).not.toContain("](");
+ });
});