diff options
Diffstat (limited to '.rules')
| -rw-r--r-- | .rules/changelog/2026-03/28/08.md | 8 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/28/09.md | 13 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/28/10.md | 15 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/29/01.md | 30 | ||||
| -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 |
14 files changed, 359 insertions, 648 deletions
diff --git a/.rules/changelog/2026-03/28/08.md b/.rules/changelog/2026-03/28/08.md deleted file mode 100644 index e5c93a4..0000000 --- a/.rules/changelog/2026-03/28/08.md +++ /dev/null @@ -1,8 +0,0 @@ -# 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/changelog/2026-03/28/09.md b/.rules/changelog/2026-03/28/09.md deleted file mode 100644 index cadb738..0000000 --- a/.rules/changelog/2026-03/28/09.md +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog — 2026-03-28 — 09 - -## Phase 1: Daily Note Manager - -### Added -- `src/calendar/daily-notes.ts` — Core daily note module with all Phase 1 functions: - - `getDailyNotePath` — computes `{rootFolder}/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md` - - `getDailyNote` — vault lookup by computed path - - `createDailyNote` — creates folders and file, supports template with `{{date}}` replacement - - `openDailyNote` — get-or-create then open in workspace leaf - - `indexDailyNotes` — recursively scans root folder, returns `Map<"YYYY-MM-DD", TFile>` - - `getDateFromDailyNote` — reverse lookup: extracts date from TFile path -- `src/global.d.ts` — Global type declaration for `window.moment` (Obsidian provides moment at runtime but does not export types for it) diff --git a/.rules/changelog/2026-03/28/10.md b/.rules/changelog/2026-03/28/10.md deleted file mode 100644 index a5f6de0..0000000 --- a/.rules/changelog/2026-03/28/10.md +++ /dev/null @@ -1,15 +0,0 @@ -# Phase 2: Calendar State - -## Added -- `src/calendar/calendar-state.ts` — observable state container for the calendar view - -## Details -- `CalendarState` class with private fields: `displayedMonth`, `today`, `activeFileDate`, `noteIndex` -- Read-only getters for all state fields -- `subscribe(cb)` / unsubscribe pattern using a `Set<() => void>` -- `setDisplayedMonth(m)` — clones and normalizes to start-of-month -- `setActiveFile(file, rootFolder)` — delegates to `getDateFromDailyNote()` from Phase 1 -- `reindex(app, rootFolder)` — delegates to `indexDailyNotes()` from Phase 1 -- `tick()` — heartbeat that notifies only on day rollover -- `rootFolder` passed as parameter (not stored) for consistency across methods -- No framework dependencies; strict TypeScript, no `any` diff --git a/.rules/changelog/2026-03/29/01.md b/.rules/changelog/2026-03/29/01.md deleted file mode 100644 index 6d037c6..0000000 --- a/.rules/changelog/2026-03/29/01.md +++ /dev/null @@ -1,30 +0,0 @@ -# Chat History Persistence & Cross-Device Sync - -## New File: `src/chat-history.ts` -- Added `toPersistableMessages()` — filters ChatMessage[] to only user/assistant messages -- Added `toRuntimeMessages()` — converts persisted messages back to ChatMessage[] for LLM context -- Re-exports `PersistedMessage` type from settings - -## Modified: `src/settings.ts` -- Added `PersistedMessage` interface (role: user | assistant, content: string) -- Added `chatHistory: PersistedMessage[]` field to `AIPulseSettings` -- Chat history is stored in `data.json` via Obsidian's `loadData()`/`saveData()`, ensuring: - - Correct file path regardless of plugin folder name vs manifest ID - - Native Obsidian Sync support - - `onExternalSettingsChange()` fires automatically on sync - -## Modified: `src/chat-view.ts` -- On **open**: restores persisted chat history from `plugin.settings.chatHistory`, renders user messages as plain text and assistant messages as rendered markdown with wiki-link navigation -- On **message send**: debounced save (500ms) after user message and after assistant response completes -- On **close**: flushes pending save and cleans up debounce timer -- On **clear chat**: sets `chatHistory` to empty array and saves settings (syncs clear to all devices) -- Added `reloadChatHistory()` public method for external sync triggers; skips reload if streaming is active -- Added `saveChatHistoryDebounced()` with snapshot update to prevent false sync reloads -- Added `renderPersistedMessages()` to re-render history with markdown and wiki-link click handlers - -## Modified: `src/main.ts` -- Added `onExternalSettingsChange()` — reloads settings and checks for chat history changes when Obsidian Sync updates `data.json` -- Added `visibilitychange` DOM event listener — reloads settings from disk when the app regains focus (covers device switching) -- Added `checkChatHistorySync()` — snapshot-based change detection that reloads the chat view only when the persisted data differs from the known state -- Added `updateChatSnapshot()` — called after local saves and restores to prevent false sync triggers -- Added `buildChatSnapshot()` helper — lightweight string comparison using message count and last message content diff --git a/.rules/plan/calendar-phase-1.md b/.rules/plan/calendar-phase-1.md deleted file mode 100644 index a1f9b2e..0000000 --- a/.rules/plan/calendar-phase-1.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index e1e22ff..0000000 --- a/.rules/plan/calendar-phase-2.md +++ /dev/null @@ -1,41 +0,0 @@ -# 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 deleted file mode 100644 index 228f7e0..0000000 --- a/.rules/plan/calendar-phase-3.md +++ /dev/null @@ -1,74 +0,0 @@ -# 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 deleted file mode 100644 index c7b2c86..0000000 --- a/.rules/plan/calendar-phase-4.md +++ /dev/null @@ -1,84 +0,0 @@ -# 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 deleted file mode 100644 index d4caf53..0000000 --- a/.rules/plan/calendar-phase-5.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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 deleted file mode 100644 index 382059a..0000000 --- a/.rules/plan/calendar-phase-6.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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 deleted file mode 100644 index 8bd0aaf..0000000 --- a/.rules/plan/calendar-phase-7.md +++ /dev/null @@ -1,109 +0,0 @@ -# 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 deleted file mode 100644 index 6b52347..0000000 --- a/.rules/plan/calendar-phase-8.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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 deleted file mode 100644 index 2d5a8cb..0000000 --- a/.rules/plan/calendar-phase-9.md +++ /dev/null @@ -1,46 +0,0 @@ -# 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 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 - |
