diff options
Diffstat (limited to '.rules/plan/calendar.md')
| -rw-r--r-- | .rules/plan/calendar.md | 394 |
1 files changed, 359 insertions, 35 deletions
diff --git a/.rules/plan/calendar.md b/.rules/plan/calendar.md index f135ae1..3a79846 100644 --- a/.rules/plan/calendar.md +++ b/.rules/plan/calendar.md @@ -6,40 +6,6 @@ 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) @@ -139,6 +105,365 @@ 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`) @@ -201,4 +526,3 @@ 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 - |
