diff options
| -rw-r--r-- | .rules/changelog/2026-03/28/08.md | 8 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-1.md | 64 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-2.md | 41 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-3.md | 74 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-4.md | 84 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-5.md | 54 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-6.md | 39 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-7.md | 109 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-8.md | 36 | ||||
| -rw-r--r-- | .rules/plan/calendar-phase-9.md | 46 | ||||
| -rw-r--r-- | .rules/plan/calendar.md | 394 |
11 files changed, 590 insertions, 359 deletions
diff --git a/.rules/changelog/2026-03/28/08.md b/.rules/changelog/2026-03/28/08.md new file mode 100644 index 0000000..e5c93a4 --- /dev/null +++ b/.rules/changelog/2026-03/28/08.md @@ -0,0 +1,8 @@ +# Changelog — 2026-03-28 #08 + +## Split calendar plan into independent phase files + +- Split monolithic `.rules/plan/calendar.md` into 9 independent phase files (`calendar-phase-1.md` through `calendar-phase-9.md`). +- Each phase file is self-contained with status, dependencies, design spec, and implementation notes. +- Rewrote `calendar.md` as an overview document with a phase index table linking to all 9 files. +- Retained shared context (goal, reference analysis, storage structure, file lists, considerations, fork analysis) in the overview. diff --git a/.rules/plan/calendar-phase-1.md b/.rules/plan/calendar-phase-1.md new file mode 100644 index 0000000..a1f9b2e --- /dev/null +++ b/.rules/plan/calendar-phase-1.md @@ -0,0 +1,64 @@ +# Phase 1: Daily Note Manager (`src/calendar/daily-notes.ts`) + +**Status:** Not started +**Depends on:** Nothing (standalone) +**Output file:** `src/calendar/daily-notes.ts` + +--- + +## Overview + +Core module — no UI, just logic. All daily note path computation, CRUD, indexing, and date detection lives here. + +--- + +## Storage Structure + +``` +{rootFolder}/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md +``` + +- **`rootFolder`** — configurable, defaults to `"Calendar"` +- **Year** — 4-digit (`2026`) +- **Month** — 2-digit zero-padded (`01`–`12`) +- **Day** — 2-digit zero-padded (`01`–`31`) +- **Filename** — `YYYY-MM-DD.md` (ISO date) + +--- + +## Functions + +``` +- getDailyNotePath(date: Moment, rootFolder: string): string + Computes: `{rootFolder}/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md` + +- getDailyNote(app: App, date: Moment, rootFolder: string): TFile | null + Looks up vault file at the computed path. + +- createDailyNote(app: App, date: Moment, rootFolder: string, template?: string): Promise<TFile> + Creates parent folders if needed, creates the file. + Uses template content if configured, else empty with frontmatter: + --- + date: YYYY-MM-DD + --- + +- openDailyNote(app: App, date: Moment, rootFolder: string, opts: { newLeaf: boolean }): Promise<void> + Opens existing note or creates then opens. + +- indexDailyNotes(app: App, rootFolder: string): Map<string, TFile> + Scans `{rootFolder}/` recursively, parses YYYY/MM/DD structure, + returns Map<"YYYY-MM-DD", TFile>. + +- getDateFromDailyNote(file: TFile, rootFolder: string): Moment | null + Reverse lookup: given a TFile, extract the date if it lives + in the daily note folder structure. +``` + +--- + +## Notes + +- `moment.js` is available globally in Obsidian as `window.moment()`. No import needed. +- `indexDailyNotes()` scans only the calendar root folder, not the entire vault. +- Template support: if `calendarDailyNoteTemplate` is set, new daily notes copy that file's content (with `{{date}}` placeholder replacement). +- The `day/` folder can hold multiple notes per day for future expansion, but the calendar UI currently shows one note per day. diff --git a/.rules/plan/calendar-phase-2.md b/.rules/plan/calendar-phase-2.md new file mode 100644 index 0000000..e1e22ff --- /dev/null +++ b/.rules/plan/calendar-phase-2.md @@ -0,0 +1,41 @@ +# Phase 2: Calendar State (`src/calendar/calendar-state.ts`) + +**Status:** Not started +**Depends on:** Phase 1 (daily-notes.ts — for `indexDailyNotes`, `getDateFromDailyNote`) +**Output file:** `src/calendar/calendar-state.ts` + +--- + +## Overview + +Simple state container with change notifications — replaces Svelte stores. Holds the displayed month, today's date, active file tracking, and the note index. Notifies subscribers on any state change. + +--- + +## Design + +``` +class CalendarState: + - displayedMonth: Moment (current month being viewed) + - today: Moment (refreshed by heartbeat) + - activeFileDate: string | null (date UID of active file, if daily note) + - noteIndex: Map<string, TFile> (date string → file) + - listeners: Set<() => void> + + Methods: + - subscribe(cb): () => void (unsubscribe function) + - setDisplayedMonth(m: Moment): void + - setActiveFile(file: TFile | null): void + - reindex(app: App, rootFolder: string): void + - tick(): void (refresh today) + - notify(): void (call all listeners) +``` + +--- + +## Notes + +- `reindex()` delegates to `indexDailyNotes()` from Phase 1. +- `setActiveFile()` uses `getDateFromDailyNote()` from Phase 1 to determine if the file is a daily note. +- `tick()` updates `today` and calls `notify()` only if the date has changed (day rollover). +- Subscribers are plain callbacks — no framework dependency. diff --git a/.rules/plan/calendar-phase-3.md b/.rules/plan/calendar-phase-3.md new file mode 100644 index 0000000..228f7e0 --- /dev/null +++ b/.rules/plan/calendar-phase-3.md @@ -0,0 +1,74 @@ +# Phase 3: Calendar Renderer (`src/calendar/calendar-renderer.ts`) + +**Status:** Not started +**Depends on:** Phase 2 (calendar-state.ts — reads state for rendering) +**Output file:** `src/calendar/calendar-renderer.ts` + +--- + +## Overview + +Pure DOM rendering — replaces `Calendar.svelte` and `obsidian-calendar-ui`. Builds the month grid using Obsidian's `createEl`/`createDiv` helpers. Subscribes to `CalendarState` and re-renders on changes. + +--- + +## Design + +``` +class CalendarRenderer: + constructor(containerEl: HTMLElement, state: CalendarState, callbacks: CalendarCallbacks) + + interface CalendarCallbacks: + onClickDay(date: Moment, event: MouseEvent): void + onClickWeek(date: Moment, event: MouseEvent): void + onClickMonth(date: Moment, event: MouseEvent): void // from fork: click month label + onClickYear(date: Moment, event: MouseEvent): void // from fork: click year label + onClickQuarter(date: Moment, event: MouseEvent): void // from fork: click quarter label + onContextMenuDay(date: Moment, event: MouseEvent): void + onContextMenuWeek(date: Moment, event: MouseEvent): void + onContextMenuMonth(date: Moment, event: MouseEvent): void // from fork + onContextMenuYear(date: Moment, event: MouseEvent): void // from fork + onContextMenuQuarter(date: Moment, event: MouseEvent): void // from fork + onHoverDay(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void + onHoverWeek(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void + onHoverMonth(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork + onHoverYear(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork + onHoverQuarter(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork + + Methods: + - render(): void + Clears containerEl, builds: + - Navigation bar: [<] [Month Year] [>] [Today] + - Month and Year labels are clickable → callbacks.onClickMonth / onClickYear (from fork) + - Quarter label (e.g. Q1) shown if calendarShowQuarter is true (from fork) + - Weekday headers row (Mon, Tue, ...) + - Optional week number column (left or right based on calendarShowWeekNumbersRight — from fork) + - 6 rows × 7 day cells + Each day cell: + - CSS class: "today", "has-note", "active", "other-month" + - Dots container (word count, tasks — uses settings from Phase 5) + - Click handler → callbacks.onClickDay + - Context menu → callbacks.onContextMenuDay + + - destroy(): void + Cleanup intervals, event listeners + + private: + - renderNavBar(): HTMLElement + - renderDayHeaders(): HTMLElement + - renderWeeks(): HTMLElement + - renderDay(date: Moment): HTMLElement + - renderWeekNumber(date: Moment, position: "left" | "right"): HTMLElement // from fork: position option + - renderQuarterLabel(date: Moment): HTMLElement // from fork + - getDaysInMonthGrid(month: Moment): Moment[][] + Returns 6 rows of 7 days, padding with prev/next month days +``` + +--- + +## Notes + +- All rendering uses Obsidian's `createEl`/`createDiv` helpers — no innerHTML. +- CSS classes follow the `ai-pulse-calendar-*` naming convention (see Phase 9). +- The renderer needs access to settings (week start, show week numbers, show quarter, words per dot) — these are passed via constructor or a settings reference. +- Word count dots: read the file's cached metadata or content to count words, show N dots where N = floor(wordCount / wordsPerDot), capped at a reasonable max. diff --git a/.rules/plan/calendar-phase-4.md b/.rules/plan/calendar-phase-4.md new file mode 100644 index 0000000..c7b2c86 --- /dev/null +++ b/.rules/plan/calendar-phase-4.md @@ -0,0 +1,84 @@ +# Phase 4: Calendar View (`src/calendar/calendar-view.ts`) + +**Status:** Not started +**Depends on:** Phase 1 (daily-notes.ts), Phase 2 (calendar-state.ts), Phase 3 (calendar-renderer.ts) +**Output file:** `src/calendar/calendar-view.ts` + +--- + +## Overview + +The `ItemView` subclass — wires everything together. Creates the state and renderer, registers vault/workspace events, and implements all callback handlers for user interactions. + +--- + +## Design + +``` +VIEW_TYPE_CALENDAR = "ai-pulse-calendar" + +class CalendarView extends ItemView: + - state: CalendarState + - renderer: CalendarRenderer + + getViewType(): "ai-pulse-calendar" + getDisplayText(): "Calendar" + getIcon(): "calendar" + + onOpen(): + - Initialize state (today, reindex notes) + - Create renderer with callbacks + - Register vault events (create, delete, modify, rename) → reindex + re-render + - Register workspace events (file-open) → update active file highlight + - Start 60s heartbeat for day rollover + - Initial render + + onClose(): + - renderer.destroy() + - Clear intervals + + Callbacks: + onClickDay(date, event): + - If note exists → open it (respecting Ctrl+Click behavior setting — from fork) + - If not → create (with optional confirmation modal) then open + + onClickWeek(date, event): + - If weekly note exists → open it (future expansion) + - Same Ctrl+Click behavior setting (from fork) + + onClickMonth(date, event): // from fork + - If monthly note exists → open it + - If not → create then open (future expansion) + + onClickYear(date, event): // from fork + - If yearly note exists → open it + - If not → create then open (future expansion) + + onClickQuarter(date, event): // from fork + - If quarterly note exists → open it + - If not → create then open (future expansion) + + onContextMenuDay(date, event): + - Show file menu (delete, open in new tab, etc.) if note exists + + onContextMenuWeek/Month/Year/Quarter(date, event): // from fork + - Show file menu if note exists for that period + + onHoverDay/Week/Month/Year/Quarter(date, targetEl, isMetaPressed): // from fork + - Trigger link-hover for page preview when Ctrl/Cmd held + + revealActiveNote(): + - If active file is a daily note, set displayedMonth to that date + - Also check weekly, monthly, quarterly, and yearly note formats (from fork) +``` + +--- + +## Notes + +- Vault events to listen for: `create`, `delete`, `modify`, `rename` — all trigger `state.reindex()` + `renderer.render()`. +- The `file-open` workspace event updates `state.setActiveFile()`. +- Ctrl+Click behavior is controlled by the `calendarCtrlClickOpensInNewTab` setting (Phase 5). +- The confirmation modal before creating a note is controlled by `calendarConfirmBeforeCreate` (Phase 5). +- Week/month/year/quarter click handlers are initially no-ops (future expansion) except for daily notes. +- Hover preview uses Obsidian's `link-hover` workspace trigger. diff --git a/.rules/plan/calendar-phase-5.md b/.rules/plan/calendar-phase-5.md new file mode 100644 index 0000000..d4caf53 --- /dev/null +++ b/.rules/plan/calendar-phase-5.md @@ -0,0 +1,54 @@ +# Phase 5: Settings Integration (`src/calendar/calendar-settings.ts` + `src/settings.ts`) + +**Status:** Not started +**Depends on:** Nothing (can be implemented independently, but wired in Phase 6) +**Output files:** `src/calendar/calendar-settings.ts`, modifications to `src/settings.ts` + +--- + +## Overview + +Add calendar-specific settings to the plugin's settings interface and settings UI. + +--- + +## New Settings Fields + +Add to `AIPulseSettings` interface in `src/settings.ts`: + +``` +- calendarRootFolder: string (default: "Calendar") +- calendarConfirmBeforeCreate: boolean (default: true) +- calendarWeekStart: "locale" | "sunday" | "monday" | ... (default: "locale") +- calendarShowWeekNumbers: boolean (default: false) +- calendarShowWeekNumbersRight: boolean (default: false) — from fork: option to display week numbers on the right side +- calendarShowQuarter: boolean (default: false) — from fork: toggle quarter display (Q1–Q4) +- calendarCtrlClickOpensInNewTab: boolean (default: false) — from fork: Ctrl+Click opens in new tab instead of new split +- calendarShowWordCountDots: boolean (default: true) +- calendarWordsPerDot: number (default: 250) +- calendarDailyNoteTemplate: string (default: "") +``` + +--- + +## Settings UI (`CalendarSettingsSection`) + +In `src/calendar/calendar-settings.ts`, create a function or class that adds a "Calendar" section to the settings modal: + +- Root folder picker (text field) +- Week start dropdown +- Confirm before create toggle +- Ctrl+Click behavior dropdown ("Open in new tab" vs "Open in new split") — from fork +- Show week numbers toggle +- Show week numbers on right side toggle — from fork +- Show quarter toggle — from fork +- Word count dots toggle + words per dot number +- Daily note template path (text field) + +--- + +## Notes + +- Settings must be added to the default settings object so existing users get sane defaults on upgrade. +- The settings UI section should be visually grouped under a "Calendar" heading in the settings tab. +- The template path field should accept a vault-relative path to a markdown file. diff --git a/.rules/plan/calendar-phase-6.md b/.rules/plan/calendar-phase-6.md new file mode 100644 index 0000000..382059a --- /dev/null +++ b/.rules/plan/calendar-phase-6.md @@ -0,0 +1,39 @@ +# Phase 6: Main Plugin Wiring (`src/main.ts`) + +**Status:** Not started +**Depends on:** Phase 4 (calendar-view.ts), Phase 5 (settings) +**Modifies:** `src/main.ts` + +--- + +## Overview + +Wire the calendar view into the main plugin class: register the view, add ribbon icon, register commands, handle lifecycle. + +--- + +## Changes to `src/main.ts` + +``` +In onload(): + - registerView(VIEW_TYPE_CALENDAR, (leaf) => new CalendarView(leaf, this)) + - addRibbonIcon("calendar", "Open Calendar", () => activateCalendarView()) + - addCommand("open-calendar", "Open Calendar View", ...) + - addCommand("reveal-active-note", "Reveal active note in calendar", ...) + - addCommand("open-today", "Open today's daily note", ...) + +In onunload(): + - detachLeavesOfType(VIEW_TYPE_CALENDAR) + +activateCalendarView(): + - Same pattern as activateView() for the chat — check for existing leaf first +``` + +--- + +## Notes + +- Follow the existing pattern in `main.ts` for registering views (look at how the chat view is registered). +- The `activateCalendarView()` helper should check if a calendar leaf already exists before creating a new one. +- Calendar settings must be included in `loadData()`/`saveData()` — merge with defaults. +- The "Open today's daily note" command uses `openDailyNote()` from Phase 1 with `window.moment()` as the date. diff --git a/.rules/plan/calendar-phase-7.md b/.rules/plan/calendar-phase-7.md new file mode 100644 index 0000000..8bd0aaf --- /dev/null +++ b/.rules/plan/calendar-phase-7.md @@ -0,0 +1,109 @@ +# Phase 7: AI Tools for Date-Based Notes + +**Status:** Not started +**Depends on:** Phase 1 (daily-notes.ts) +**Output files:** `src/context/tools/read-daily-note.json`, `src/context/tools/write-daily-note.json`, modifications to `src/tools.ts` + +--- + +## Overview + +Two new tools the AI can use to interact with the calendar structure: `read_daily_note` and `write_daily_note`. + +--- + +## `read_daily_note` Tool + +### JSON Definition (`src/context/tools/read-daily-note.json`) + +```json +{ + "id": "read_daily_note", + "label": "Read Daily Note", + "description": "Read the daily note for a specific date", + "friendlyName": "Read Daily Note", + "requiresApproval": false, + "definition": { + "type": "function", + "function": { + "name": "read_daily_note", + "description": "Read the daily note for a given date. Use 'today' for the current date, or provide a date in YYYY-MM-DD format.", + "parameters": { + "type": "object", + "required": ["date"], + "properties": { + "date": { + "type": "string", + "description": "The date to read. Use 'today', 'yesterday', 'tomorrow', or a YYYY-MM-DD date string." + } + } + } + } + } +} +``` + +### Execute Logic + +1. Parse the `date` argument — handle `"today"`, `"yesterday"`, `"tomorrow"`, or parse `YYYY-MM-DD` with `moment()` +2. Compute the path using `getDailyNotePath()` +3. If file exists: read and return content (same format as `read_file`) +4. If not: return `"No daily note exists for {date}."` + +--- + +## `write_daily_note` Tool + +### JSON Definition (`src/context/tools/write-daily-note.json`) + +```json +{ + "id": "write_daily_note", + "label": "Write Daily Note", + "description": "Write or append to the daily note for a specific date", + "friendlyName": "Write Daily Note", + "requiresApproval": true, + "definition": { + "type": "function", + "function": { + "name": "write_daily_note", + "description": "Write content to the daily note for a given date. Creates the note if it does not exist. Use mode 'append' to add to the end, or 'overwrite' to replace all content.", + "parameters": { + "type": "object", + "required": ["date", "content"], + "properties": { + "date": { + "type": "string", + "description": "The date to write to. Use 'today', 'yesterday', 'tomorrow', or a YYYY-MM-DD date string." + }, + "content": { + "type": "string", + "description": "The content to write." + }, + "mode": { + "type": "string", + "description": "Write mode: 'append' (default) adds to the end, 'overwrite' replaces all content." + } + } + } + } + } +} +``` + +### Execute Logic + +1. Parse date (same as read) +2. If file does not exist: create it with content (using `createDailyNote()` then write) +3. If file exists: + - `"append"` (default): `app.vault.append(file, "\n" + content)` + - `"overwrite"`: `app.vault.modify(file, content)` +4. Return confirmation message + +--- + +## Registration in `src/tools.ts` + +- Import both JSON files +- Add `TOOL_REGISTRY` entries that spread the JSON context and add runtime callbacks (`summarize`, `summarizeResult`, `execute`, and optionally `approvalMessage`) +- Follow the existing pattern for other tools diff --git a/.rules/plan/calendar-phase-8.md b/.rules/plan/calendar-phase-8.md new file mode 100644 index 0000000..6b52347 --- /dev/null +++ b/.rules/plan/calendar-phase-8.md @@ -0,0 +1,36 @@ +# Phase 8: System Prompt Update + +**Status:** Not started +**Depends on:** Phase 7 (AI tools must exist for the prompt to reference them) +**Modifies:** `src/context/system-prompt.json` + +--- + +## Overview + +Add a section to the system prompt that explains the date-based note structure to the AI, so it knows when and how to use the daily note tools. + +--- + +## Addition to `system-prompt.json` + +```json +"dailyNotes": { + "header": "DAILY NOTES — DATE-BASED NOTE STRUCTURE:", + "description": "The vault uses a calendar-based daily note system. Notes are stored at Calendar/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md.", + "tools": "Use read_daily_note and write_daily_note to interact with daily notes by date. These accept natural date references like 'today', 'yesterday', 'tomorrow', or explicit YYYY-MM-DD dates.", + "rules": [ + "When the user refers to 'today's note', 'my daily note', or a specific date, use read_daily_note or write_daily_note.", + "Do NOT use create_file or read_file for daily notes — always use the dedicated daily note tools.", + "The daily note tools handle folder creation and path computation automatically.", + "When appending to a daily note, the content is added at the end of the file." + ] +} +``` + +--- + +## Notes + +- The `rootFolder` in the description should ideally reference the actual configured value, but since the system prompt is static JSON, use the default `"Calendar"` and note that it's configurable. +- This section ensures the AI prefers the dedicated daily note tools over generic file operations for date-based notes. diff --git a/.rules/plan/calendar-phase-9.md b/.rules/plan/calendar-phase-9.md new file mode 100644 index 0000000..2d5a8cb --- /dev/null +++ b/.rules/plan/calendar-phase-9.md @@ -0,0 +1,46 @@ +# Phase 9: Calendar CSS (`styles.css`) + +**Status:** Not started +**Depends on:** Phase 3 (calendar-renderer.ts — must match the CSS classes used in rendering) +**Modifies:** `styles.css` + +--- + +## Overview + +Append calendar styles to the plugin's stylesheet. Use Obsidian CSS variables for theme compatibility (light/dark). + +--- + +## CSS Classes + +``` +.ai-pulse-calendar — container +.ai-pulse-calendar-nav — month navigation bar +.ai-pulse-calendar-nav-title — "March 2026" (clickable → opens monthly note — from fork) +.ai-pulse-calendar-nav-year — year label (clickable → opens yearly note — from fork) +.ai-pulse-calendar-nav-quarter — quarter label, e.g. "Q1" (clickable → opens quarterly note — from fork) +.ai-pulse-calendar-nav-btn — < > Today buttons +.ai-pulse-calendar-grid — the 7-column grid (8-column when week numbers shown) +.ai-pulse-calendar-weekday — header cells (Mon, Tue...) +.ai-pulse-calendar-weeknum — week number cell (from fork: can be left or right column) +.ai-pulse-calendar-day — individual day cell +.ai-pulse-calendar-day.today — today highlight +.ai-pulse-calendar-day.has-note — day with a note +.ai-pulse-calendar-day.active — currently open note's date +.ai-pulse-calendar-day.other-month — padding days from adjacent months +.ai-pulse-calendar-dots — dot container within day cell +.ai-pulse-calendar-dot — individual dot (word count) +``` + +--- + +## Notes + +- Use `var(--background-primary)`, `var(--text-normal)`, `var(--interactive-accent)`, etc. for theme compatibility. +- The grid should be responsive within the sidebar width. +- Day cells should have consistent sizing — use CSS Grid with `grid-template-columns: repeat(7, 1fr)` (or 8 when week numbers are shown). +- Today highlight should use `var(--interactive-accent)` with reduced opacity for the background. +- "has-note" dots should be small circles below the day number. +- "other-month" days should have reduced opacity. +- "active" day should have a distinct border or background to show it's the currently open note. diff --git a/.rules/plan/calendar.md b/.rules/plan/calendar.md index 3a79846..f135ae1 100644 --- a/.rules/plan/calendar.md +++ b/.rules/plan/calendar.md @@ -6,6 +6,40 @@ Incorporate the functionality of `liamcain/obsidian-calendar-plugin` into AI Pul --- +## Phase Files + +Each phase is documented in its own file for independent implementation: + +| Phase | File | Summary | +|-------|------|---------| +| 1 | [calendar-phase-1.md](calendar-phase-1.md) | Daily Note Manager — path computation, CRUD, indexing | +| 2 | [calendar-phase-2.md](calendar-phase-2.md) | Calendar State — observable state container | +| 3 | [calendar-phase-3.md](calendar-phase-3.md) | Calendar Renderer — pure DOM month grid | +| 4 | [calendar-phase-4.md](calendar-phase-4.md) | Calendar View — ItemView subclass wiring | +| 5 | [calendar-phase-5.md](calendar-phase-5.md) | Settings Integration — calendar settings UI | +| 6 | [calendar-phase-6.md](calendar-phase-6.md) | Main Plugin Wiring — commands, ribbon, lifecycle | +| 7 | [calendar-phase-7.md](calendar-phase-7.md) | AI Tools — read_daily_note, write_daily_note | +| 8 | [calendar-phase-8.md](calendar-phase-8.md) | System Prompt Update — daily notes context for AI | +| 9 | [calendar-phase-9.md](calendar-phase-9.md) | Calendar CSS — styles for the calendar grid | + +--- + +## Implementation Order + +1. **Phase 1** — `daily-notes.ts` (core logic, testable in isolation) +2. **Phase 2** — `calendar-state.ts` (state management) +3. **Phase 3** — `calendar-renderer.ts` (DOM rendering) +4. **Phase 4** — `calendar-view.ts` (ItemView wiring) +5. **Phase 5** — Settings integration +6. **Phase 6** — Main plugin wiring + commands +7. **Phase 9** — CSS styles +8. **Phase 7** — AI tools (read/write daily note) +9. **Phase 8** — System prompt update + +Phases 1–6 and 9 deliver a fully working calendar view. Phases 7–8 add the AI integration. + +--- + ## Reference Plugin Analysis ### What It Does (Features to Keep) @@ -105,365 +139,6 @@ Calendar/ --- -## Implementation Phases - -### Phase 1: Daily Note Manager (`src/calendar/daily-notes.ts`) - -Core module — no UI, just logic. - -``` -Functions: - - getDailyNotePath(date: Moment, rootFolder: string): string - Computes: `{rootFolder}/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md` - - - getDailyNote(app: App, date: Moment, rootFolder: string): TFile | null - Looks up vault file at the computed path. - - - createDailyNote(app: App, date: Moment, rootFolder: string, template?: string): Promise<TFile> - Creates parent folders if needed, creates the file. - Uses template content if configured, else empty with frontmatter: - --- - date: YYYY-MM-DD - --- - - - openDailyNote(app: App, date: Moment, rootFolder: string, opts: { newLeaf: boolean }): Promise<void> - Opens existing note or creates then opens. - - - indexDailyNotes(app: App, rootFolder: string): Map<string, TFile> - Scans `{rootFolder}/` recursively, parses YYYY/MM/DD structure, - returns Map<"YYYY-MM-DD", TFile>. - - - getDateFromDailyNote(file: TFile, rootFolder: string): Moment | null - Reverse lookup: given a TFile, extract the date if it lives - in the daily note folder structure. -``` - -### Phase 2: Calendar State (`src/calendar/calendar-state.ts`) - -Simple state container with change notifications — replaces Svelte stores. - -``` -class CalendarState: - - displayedMonth: Moment (current month being viewed) - - today: Moment (refreshed by heartbeat) - - activeFileDate: string | null (date UID of active file, if daily note) - - noteIndex: Map<string, TFile> (date string → file) - - listeners: Set<() => void> - - Methods: - - subscribe(cb): () => void (unsubscribe function) - - setDisplayedMonth(m: Moment): void - - setActiveFile(file: TFile | null): void - - reindex(app: App, rootFolder: string): void - - tick(): void (refresh today) - - notify(): void (call all listeners) -``` - -### Phase 3: Calendar Renderer (`src/calendar/calendar-renderer.ts`) - -Pure DOM rendering — replaces `Calendar.svelte` and `obsidian-calendar-ui`. - -``` -class CalendarRenderer: - constructor(containerEl: HTMLElement, state: CalendarState, callbacks: CalendarCallbacks) - - interface CalendarCallbacks: - onClickDay(date: Moment, event: MouseEvent): void - onClickWeek(date: Moment, event: MouseEvent): void - onClickMonth(date: Moment, event: MouseEvent): void // from fork: click month label - onClickYear(date: Moment, event: MouseEvent): void // from fork: click year label - onClickQuarter(date: Moment, event: MouseEvent): void // from fork: click quarter label - onContextMenuDay(date: Moment, event: MouseEvent): void - onContextMenuWeek(date: Moment, event: MouseEvent): void - onContextMenuMonth(date: Moment, event: MouseEvent): void // from fork - onContextMenuYear(date: Moment, event: MouseEvent): void // from fork - onContextMenuQuarter(date: Moment, event: MouseEvent): void // from fork - onHoverDay(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void - onHoverWeek(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void - onHoverMonth(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork - onHoverYear(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork - onHoverQuarter(date: Moment, targetEl: EventTarget, isMetaPressed: boolean): void // from fork - - Methods: - - render(): void - Clears containerEl, builds: - - Navigation bar: [<] [Month Year] [>] [Today] - - Month and Year labels are clickable → callbacks.onClickMonth / onClickYear (from fork) - - Quarter label (e.g. Q1) shown if calendarShowQuarter is true (from fork) - - Weekday headers row (Mon, Tue, ...) - - Optional week number column (left or right based on calendarShowWeekNumbersRight — from fork) - - 6 rows × 7 day cells - Each day cell: - - CSS class: "today", "has-note", "active", "other-month" - - Dots container (word count, tasks — Phase 5) - - Click handler → callbacks.onClickDay - - Context menu → callbacks.onContextMenuDay - - - destroy(): void - Cleanup intervals, event listeners - - private: - - renderNavBar(): HTMLElement - - renderDayHeaders(): HTMLElement - - renderWeeks(): HTMLElement - - renderDay(date: Moment): HTMLElement - - renderWeekNumber(date: Moment, position: "left" | "right"): HTMLElement // from fork: position option - - renderQuarterLabel(date: Moment): HTMLElement // from fork - - getDaysInMonthGrid(month: Moment): Moment[][] - Returns 6 rows of 7 days, padding with prev/next month days -``` - -### Phase 4: Calendar View (`src/calendar/calendar-view.ts`) - -The `ItemView` subclass — wires everything together. Replaces `view.ts`. - -``` -VIEW_TYPE_CALENDAR = "ai-pulse-calendar" - -class CalendarView extends ItemView: - - state: CalendarState - - renderer: CalendarRenderer - - getViewType(): "ai-pulse-calendar" - getDisplayText(): "Calendar" - getIcon(): "calendar" - - onOpen(): - - Initialize state (today, reindex notes) - - Create renderer with callbacks - - Register vault events (create, delete, modify, rename) → reindex + re-render - - Register workspace events (file-open) → update active file highlight - - Start 60s heartbeat for day rollover - - Initial render - - onClose(): - - renderer.destroy() - - Clear intervals - - Callbacks: - onClickDay(date, event): - - If note exists → open it (respecting Ctrl+Click behavior setting — from fork) - - If not → create (with optional confirmation modal) then open - - onClickWeek(date, event): - - If weekly note exists → open it (future expansion) - - Same Ctrl+Click behavior setting (from fork) - - onClickMonth(date, event): // from fork - - If monthly note exists → open it - - If not → create then open (future expansion) - - onClickYear(date, event): // from fork - - If yearly note exists → open it - - If not → create then open (future expansion) - - onClickQuarter(date, event): // from fork - - If quarterly note exists → open it - - If not → create then open (future expansion) - - onContextMenuDay(date, event): - - Show file menu (delete, open in new tab, etc.) if note exists - - onContextMenuWeek/Month/Year/Quarter(date, event): // from fork - - Show file menu if note exists for that period - - onHoverDay/Week/Month/Year/Quarter(date, targetEl, isMetaPressed): // from fork - - Trigger link-hover for page preview when Ctrl/Cmd held - - revealActiveNote(): - - If active file is a daily note, set displayedMonth to that date - - Also check weekly, monthly, quarterly, and yearly note formats (from fork) -``` - -### Phase 5: Settings Integration (`src/calendar/calendar-settings.ts` + `src/settings.ts`) - -Add calendar-specific settings to the plugin. - -``` -New settings fields in AIPulseSettings: - - calendarRootFolder: string (default: "Calendar") - - calendarConfirmBeforeCreate: boolean (default: true) - - calendarWeekStart: "locale" | "sunday" | "monday" | ... (default: "locale") - - calendarShowWeekNumbers: boolean (default: false) - - calendarShowWeekNumbersRight: boolean (default: false) — from fork: option to display week numbers on the right side - - calendarShowQuarter: boolean (default: false) — from fork: toggle quarter display (Q1–Q4) - - calendarCtrlClickOpensInNewTab: boolean (default: false) — from fork: Ctrl+Click opens in new tab instead of new split - - calendarShowWordCountDots: boolean (default: true) - - calendarWordsPerDot: number (default: 250) - - calendarDailyNoteTemplate: string (default: "") - -CalendarSettingsSection: - - Adds a "Calendar" section to the settings modal - - Root folder picker (text field) - - Week start dropdown - - Confirm before create toggle - - Ctrl+Click behavior dropdown ("Open in new tab" vs "Open in new split") — from fork - - Show week numbers toggle - - Show week numbers on right side toggle — from fork - - Show quarter toggle — from fork - - Word count dots toggle + words per dot number - - Daily note template path (text field) -``` - -### Phase 6: Main Plugin Wiring (`src/main.ts`) - -``` -In onload(): - - registerView(VIEW_TYPE_CALENDAR, (leaf) => new CalendarView(leaf, this)) - - addRibbonIcon("calendar", "Open Calendar", () => activateCalendarView()) - - addCommand("open-calendar", "Open Calendar View", ...) - - addCommand("reveal-active-note", "Reveal active note in calendar", ...) - - addCommand("open-today", "Open today's daily note", ...) - -In onunload(): - - detachLeavesOfType(VIEW_TYPE_CALENDAR) - -activateCalendarView(): - - Same pattern as activateView() for the chat — check for existing leaf first -``` - -### Phase 7: AI Tools for Date-Based Notes - -Two new tools the AI can use to interact with the calendar structure. - -#### `read_daily_note` tool -```json -{ - "id": "read_daily_note", - "label": "Read Daily Note", - "description": "Read the daily note for a specific date", - "friendlyName": "Read Daily Note", - "requiresApproval": false, - "definition": { - "type": "function", - "function": { - "name": "read_daily_note", - "description": "Read the daily note for a given date. Use 'today' for the current date, or provide a date in YYYY-MM-DD format.", - "parameters": { - "type": "object", - "required": ["date"], - "properties": { - "date": { - "type": "string", - "description": "The date to read. Use 'today', 'yesterday', 'tomorrow', or a YYYY-MM-DD date string." - } - } - } - } - } -} -``` - -**Execute logic:** -1. Parse the `date` argument — handle `"today"`, `"yesterday"`, `"tomorrow"`, or parse `YYYY-MM-DD` with `moment()` -2. Compute the path using `getDailyNotePath()` -3. If file exists: read and return content (same format as `read_file`) -4. If not: return `"No daily note exists for {date}."` - -#### `write_daily_note` tool -```json -{ - "id": "write_daily_note", - "label": "Write Daily Note", - "description": "Write or append to the daily note for a specific date", - "friendlyName": "Write Daily Note", - "requiresApproval": true, - "definition": { - "type": "function", - "function": { - "name": "write_daily_note", - "description": "Write content to the daily note for a given date. Creates the note if it does not exist. Use mode 'append' to add to the end, or 'overwrite' to replace all content.", - "parameters": { - "type": "object", - "required": ["date", "content"], - "properties": { - "date": { - "type": "string", - "description": "The date to write to. Use 'today', 'yesterday', 'tomorrow', or a YYYY-MM-DD date string." - }, - "content": { - "type": "string", - "description": "The content to write." - }, - "mode": { - "type": "string", - "description": "Write mode: 'append' (default) adds to the end, 'overwrite' replaces all content." - } - } - } - } - } -} -``` - -**Execute logic:** -1. Parse date (same as read) -2. If file does not exist: create it with content (using `createDailyNote()` then write) -3. If file exists: - - `"append"` (default): `app.vault.append(file, "\n" + content)` - - `"overwrite"`: `app.vault.modify(file, content)` -4. Return confirmation message - -### Phase 8: System Prompt Update - -Add to `system-prompt.json`: - -``` -"dailyNotes": { - "header": "DAILY NOTES — DATE-BASED NOTE STRUCTURE:", - "description": "The vault uses a calendar-based daily note system. Notes are stored at Calendar/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md.", - "tools": "Use read_daily_note and write_daily_note to interact with daily notes by date. These accept natural date references like 'today', 'yesterday', 'tomorrow', or explicit YYYY-MM-DD dates.", - "rules": [ - "When the user refers to 'today's note', 'my daily note', or a specific date, use read_daily_note or write_daily_note.", - "Do NOT use create_file or read_file for daily notes — always use the dedicated daily note tools.", - "The daily note tools handle folder creation and path computation automatically.", - "When appending to a daily note, the content is added at the end of the file." - ] -} -``` - -### Phase 9: Calendar CSS (`styles.css`) - -Append calendar styles. Use Obsidian CSS variables for theme compatibility. - -``` -Key classes: - .ai-pulse-calendar — container - .ai-pulse-calendar-nav — month navigation bar - .ai-pulse-calendar-nav-title — "March 2026" (clickable → opens monthly note — from fork) - .ai-pulse-calendar-nav-year — year label (clickable → opens yearly note — from fork) - .ai-pulse-calendar-nav-quarter — quarter label, e.g. "Q1" (clickable → opens quarterly note — from fork) - .ai-pulse-calendar-nav-btn — < > Today buttons - .ai-pulse-calendar-grid — the 7-column grid (8-column when week numbers shown) - .ai-pulse-calendar-weekday — header cells (Mon, Tue...) - .ai-pulse-calendar-weeknum — week number cell (from fork: can be left or right column) - .ai-pulse-calendar-day — individual day cell - .ai-pulse-calendar-day.today — today highlight - .ai-pulse-calendar-day.has-note — day with a note - .ai-pulse-calendar-day.active — currently open note's date - .ai-pulse-calendar-day.other-month — padding days from adjacent months - .ai-pulse-calendar-dots — dot container within day cell - .ai-pulse-calendar-dot — individual dot (word count) -``` - ---- - -## Implementation Order - -1. **Phase 1** — `daily-notes.ts` (core logic, testable in isolation) -2. **Phase 2** — `calendar-state.ts` (state management) -3. **Phase 3** — `calendar-renderer.ts` (DOM rendering) -4. **Phase 4** — `calendar-view.ts` (ItemView wiring) -5. **Phase 5** — Settings integration -6. **Phase 6** — Main plugin wiring + commands -7. **Phase 9** — CSS styles -8. **Phase 7** — AI tools (read/write daily note) -9. **Phase 8** — System prompt update - -Phases 1–7 and 9 deliver a fully working calendar view. Phases 7–8 add the AI integration. - ---- - ## Considerations - **No new dependencies** — everything is built with Obsidian API + DOM + `moment` (already available globally via `window.moment`) @@ -526,3 +201,4 @@ The `Calendar.svelte` component passes additional props to `CalendarBase`: ### What We Defer - **Monthly/quarterly/yearly note CRUD** — our storage structure is different; we'll add these later as the calendar matures - **Monthly/quarterly/yearly stores and reindexing** — not needed until those note types are implemented + |
