diff options
Diffstat (limited to '.rules/plan/calendar.md')
| -rw-r--r-- | .rules/plan/calendar.md | 394 |
1 files changed, 35 insertions, 359 deletions
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 + |
