summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.rules/changelog/2026-03/28/08.md8
-rw-r--r--.rules/changelog/2026-03/28/09.md13
-rw-r--r--.rules/changelog/2026-03/28/10.md15
-rw-r--r--.rules/changelog/2026-03/29/01.md30
-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
-rw-r--r--src/chat-history.ts24
-rw-r--r--src/chat-view.ts110
-rw-r--r--src/global.d.ts7
-rw-r--r--src/main.ts69
-rw-r--r--src/settings.ts12
19 files changed, 359 insertions, 870 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
-
diff --git a/src/chat-history.ts b/src/chat-history.ts
deleted file mode 100644
index ac7c6a8..0000000
--- a/src/chat-history.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { ChatMessage } from "./ollama-client";
-import type { PersistedMessage } from "./settings";
-
-export type { PersistedMessage } from "./settings";
-
-/**
- * Filter ChatMessage[] down to only persistable user/assistant messages.
- */
-export function toPersistableMessages(messages: readonly ChatMessage[]): PersistedMessage[] {
- const result: PersistedMessage[] = [];
- for (const msg of messages) {
- if (msg.role === "user" || msg.role === "assistant") {
- result.push({ role: msg.role, content: msg.content });
- }
- }
- return result;
-}
-
-/**
- * Convert persisted messages back to ChatMessage[] for the LLM context.
- */
-export function toRuntimeMessages(messages: readonly PersistedMessage[]): ChatMessage[] {
- return messages.map((m) => ({ role: m.role, content: m.content }));
-}
diff --git a/src/chat-view.ts b/src/chat-view.ts
index dd27c63..55b730d 100644
--- a/src/chat-view.ts
+++ b/src/chat-view.ts
@@ -7,8 +7,6 @@ import { ToolModal } from "./tool-modal";
import { TOOL_REGISTRY } from "./tools";
import type { OllamaToolDefinition } from "./tools";
import { collectVaultContext, formatVaultContext } from "./vault-context";
-import { toRuntimeMessages, toPersistableMessages } from "./chat-history";
-import type { PersistedMessage } from "./chat-history";
export const VIEW_TYPE_CHAT = "ai-pulse-chat";
@@ -21,7 +19,6 @@ export class ChatView extends ItemView {
private toolsButton: HTMLButtonElement | null = null;
private abortController: AbortController | null = null;
private scrollDebounceTimer: ReturnType<typeof setTimeout> | null = null;
- private saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;
private bubbleContent: Map<HTMLDivElement, string> = new Map();
private modelBadge: HTMLDivElement | null = null;
@@ -116,9 +113,6 @@ export class ChatView extends ItemView {
if (this.messageContainer !== null) {
this.messageContainer.empty();
}
- this.plugin.settings.chatHistory = [];
- this.plugin.updateChatSnapshot([]);
- void this.plugin.saveSettings();
(document.activeElement as HTMLElement)?.blur();
});
@@ -148,22 +142,12 @@ export class ChatView extends ItemView {
// Auto-connect on open
void this.plugin.connect();
-
- // Restore persisted chat history
- void this.restoreChatHistory();
}
async onClose(): Promise<void> {
if (this.abortController !== null) {
this.abortController.abort();
}
- if (this.saveDebounceTimer !== null) {
- clearTimeout(this.saveDebounceTimer);
- this.saveDebounceTimer = null;
- }
- // Save any pending history before closing
- this.plugin.settings.chatHistory = toPersistableMessages(this.messages);
- void this.plugin.saveSettings();
this.contentEl.empty();
this.messages = [];
this.bubbleContent.clear();
@@ -235,7 +219,6 @@ export class ChatView extends ItemView {
// Track in message history
this.messages.push({ role: "user", content: text });
- this.saveChatHistoryDebounced();
// Switch to streaming state
this.abortController = new AbortController();
@@ -337,7 +320,6 @@ export class ChatView extends ItemView {
await this.finalizeBubble(currentBubble);
}
this.messages.push({ role: "assistant", content: response });
- this.saveChatHistoryDebounced();
this.scrollToBottom();
} catch (err: unknown) {
const isAbort = err instanceof DOMException && err.name === "AbortError";
@@ -755,98 +737,6 @@ export class ChatView extends ItemView {
}
}
- /**
- * Save chat history with debouncing to avoid excessive writes.
- */
- private saveChatHistoryDebounced(): void {
- if (this.saveDebounceTimer !== null) {
- clearTimeout(this.saveDebounceTimer);
- }
- this.saveDebounceTimer = setTimeout(() => {
- this.saveDebounceTimer = null;
- const persistable = toPersistableMessages(this.messages);
- this.plugin.settings.chatHistory = persistable;
- // Update the plugin's snapshot so the sync checker doesn't treat
- // our own save as an external change.
- this.plugin.updateChatSnapshot(persistable);
- void this.plugin.saveSettings();
- }, 500);
- }
-
- /**
- * Restore chat history from the persisted file and render messages.
- */
- private async restoreChatHistory(): Promise<void> {
- const persisted = this.plugin.settings.chatHistory;
- if (persisted.length === 0) return;
-
- this.messages = toRuntimeMessages(persisted);
- this.plugin.updateChatSnapshot(persisted);
- await this.renderPersistedMessages(persisted);
- this.scrollToBottom();
- }
-
- /**
- * Render persisted messages into the chat container.
- * User messages are shown as plain text; assistant messages are rendered as markdown.
- */
- private async renderPersistedMessages(messages: PersistedMessage[]): Promise<void> {
- if (this.messageContainer === null) return;
-
- for (const msg of messages) {
- if (msg.role === "user") {
- this.messageContainer.createDiv({
- cls: "ai-pulse-message user",
- text: msg.content,
- });
- } else if (msg.role === "assistant") {
- const bubble = this.messageContainer.createDiv({
- cls: "ai-pulse-message assistant ai-pulse-markdown",
- });
- await MarkdownRenderer.render(
- this.plugin.app,
- msg.content,
- bubble,
- "",
- this,
- );
-
- // Wire up internal [[wiki-links]] so they navigate on click
- bubble.querySelectorAll("a.internal-link").forEach((link) => {
- link.addEventListener("click", (evt) => {
- evt.preventDefault();
- const href = link.getAttribute("href");
- if (href !== null) {
- void this.plugin.app.workspace.openLinkText(href, "", false);
- }
- });
- });
- }
- }
- }
-
- /**
- * Reload chat history from disk (e.g., after an external sync).
- * Replaces the current messages and re-renders the UI.
- */
- async reloadChatHistory(): Promise<void> {
- const persisted = this.plugin.settings.chatHistory;
-
- // Skip reload if we're currently streaming — avoid disrupting the UI
- if (this.abortController !== null) return;
-
- this.messages = toRuntimeMessages(persisted);
- this.bubbleContent.clear();
- if (this.messageContainer !== null) {
- this.messageContainer.empty();
- }
-
- if (persisted.length > 0) {
- await this.renderPersistedMessages(persisted);
- this.scrollToBottom();
- }
- }
-
private scrollToBottom(): void {
if (this.messageContainer === null) return;
const lastChild = this.messageContainer.lastElementChild;
diff --git a/src/global.d.ts b/src/global.d.ts
deleted file mode 100644
index bc5e7ae..0000000
--- a/src/global.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type _moment from "moment";
-
-declare global {
- interface Window {
- moment: typeof _moment;
- }
-}
diff --git a/src/main.ts b/src/main.ts
index e09cf56..12dadaf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,7 +4,6 @@ import { DEFAULT_SETTINGS } from "./settings";
import { ChatView, VIEW_TYPE_CHAT } from "./chat-view";
import { testConnection, listModels } from "./ollama-client";
import { getDefaultToolStates } from "./tools";
-import type { PersistedMessage } from "./chat-history";
export default class AIPulse extends Plugin {
settings: AIPulseSettings = DEFAULT_SETTINGS;
@@ -14,9 +13,6 @@ export default class AIPulse extends Plugin {
connectionMessage = "";
availableModels: string[] = [];
- // Snapshot of persisted chat history for sync change detection
- private lastChatSnapshot = "";
-
async onload(): Promise<void> {
await this.loadSettings();
@@ -33,18 +29,6 @@ export default class AIPulse extends Plugin {
void this.activateView();
},
});
-
- // Detect chat history changes from Obsidian Sync or other devices.
- // We check when the app regains visibility (user switches back from another app/device).
- this.registerDomEvent(document, "visibilitychange", () => {
- if (document.visibilityState === "visible") {
- // Reload settings from disk in case Obsidian Sync updated data.json
- // while the app was in the background.
- void this.loadSettings().then(() => {
- this.checkChatHistorySync();
- });
- }
- });
}
onunload(): void {
@@ -88,47 +72,6 @@ export default class AIPulse extends Plugin {
await this.saveData(this.settings);
}
- /**
- * Called by Obsidian when data.json is modified externally (e.g., via Sync).
- * Reloads settings (which now include chat history) and syncs the chat view.
- */
- async onExternalSettingsChange(): Promise<void> {
- await this.loadSettings();
- this.checkChatHistorySync();
- }
-
- /**
- * Check if the persisted chat history has changed (e.g., from another device)
- * and reload the chat view if needed.
- */
- checkChatHistorySync(): void {
- try {
- const persisted = this.settings.chatHistory;
- const snapshot = buildChatSnapshot(persisted);
-
- if (snapshot === this.lastChatSnapshot) return;
- this.lastChatSnapshot = snapshot;
-
- // Find the active chat view and reload it
- const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_CHAT);
- for (const leaf of leaves) {
- const view = leaf.view;
- if (view instanceof ChatView) {
- void view.reloadChatHistory();
- }
- }
- } catch {
- // Silently ignore — sync check is best-effort
- }
- }
-
- /**
- * Update the snapshot after a local save so we don't trigger a false reload.
- */
- updateChatSnapshot(messages: PersistedMessage[]): void {
- this.lastChatSnapshot = buildChatSnapshot(messages);
- }
-
async connect(): Promise<void> {
this.connectionStatus = "connecting";
this.connectionMessage = "Connecting...";
@@ -156,15 +99,3 @@ export default class AIPulse extends Plugin {
}
}
}
-
-/**
- * Build a lightweight snapshot string of chat messages for change detection.
- * Uses message count + last message content hash to detect changes
- * without deep comparison.
- */
-function buildChatSnapshot(messages: PersistedMessage[]): string {
- if (messages.length === 0) return "empty";
- const last = messages[messages.length - 1];
- if (last === undefined) return "empty";
- return `${messages.length}:${last.role}:${last.content.length}:${last.content.slice(0, 100)}`;
-}
diff --git a/src/settings.ts b/src/settings.ts
index ab20416..c61af9d 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -1,15 +1,5 @@
import { getDefaultToolStates } from "./tools";
-/**
- * A message stored in the persisted chat history.
- * Only user and assistant messages are persisted — system and tool messages
- * are transient (injected per-request by the agent loop).
- */
-export interface PersistedMessage {
- role: "user" | "assistant";
- content: string;
-}
-
export interface AIPulseSettings {
ollamaUrl: string;
model: string;
@@ -21,7 +11,6 @@ export interface AIPulseSettings {
systemPromptFile: string;
injectVaultContext: boolean;
vaultContextRecentFiles: number;
- chatHistory: PersistedMessage[];
}
export const DEFAULT_SETTINGS: AIPulseSettings = {
@@ -35,5 +24,4 @@ export const DEFAULT_SETTINGS: AIPulseSettings = {
systemPromptFile: "agent.md",
injectVaultContext: false,
vaultContextRecentFiles: 20,
- chatHistory: [],
};