summaryrefslogtreecommitdiffhomepage
path: root/.rules
diff options
context:
space:
mode:
Diffstat (limited to '.rules')
-rw-r--r--.rules/changelog/2026-03/28/08.md8
-rw-r--r--.rules/plan/calendar-phase-1.md64
-rw-r--r--.rules/plan/calendar-phase-2.md41
-rw-r--r--.rules/plan/calendar-phase-3.md74
-rw-r--r--.rules/plan/calendar-phase-4.md84
-rw-r--r--.rules/plan/calendar-phase-5.md54
-rw-r--r--.rules/plan/calendar-phase-6.md39
-rw-r--r--.rules/plan/calendar-phase-7.md109
-rw-r--r--.rules/plan/calendar-phase-8.md36
-rw-r--r--.rules/plan/calendar-phase-9.md46
-rw-r--r--.rules/plan/calendar.md394
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
+