diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f692280a..819db1d4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -67,12 +67,15 @@ Plans: **Depends on**: Phase 27 (components must exist to render into each mode) **Requirements**: DASH-01, DASH-02, DASH-03, DASH-04, REFR-01, REFR-02 **Success Criteria** (what must be TRUE): - 1. User opens a `.base` file → dashboard header shows the domain name and the view switches to collection mode, without any manual button click. - 2. User opens a paper card (`.md` with `zotero_key` in frontmatter) → dashboard header shows the paper title and the view switches to per-paper mode, without any manual button click. - 3. User opens any other file or no file → dashboard shows the existing global library overview (metric cards, OCR pipeline, Quick Actions). - 4. User switches active file via tab click or Ctrl+Tab → dashboard transitions to the correct mode within observable time (< 500ms), with the previous mode's data released from memory. - 5. When `formal-library.json` is modified externally (by sync/ocr/repair workers) → dashboard detects the change within 2 seconds and refreshes the current view with updated data, preserving the current mode. -**Plans**: TBD + 1. User opens a `.base` file → dashboard header shows the domain name and the view switches to collection mode, without any manual button click. + 2. User opens a paper card (`.md` with `zotero_key` in frontmatter) → dashboard header shows the paper title and the view switches to per-paper mode, without any manual button click. + 3. User opens any other file or no file → dashboard shows the existing global library overview (metric cards, OCR pipeline, Quick Actions). + 4. User switches active file via tab click or Ctrl+Tab → dashboard transitions to the correct mode within observable time (< 500ms), with the previous mode's data released from memory. + 5. When `formal-library.json` is modified externally (by sync/ocr/repair workers) → dashboard detects the change within 2 seconds and refreshes the current view with updated data, preserving the current mode. +**Plans**: 2 plans +Plans: +- [ ] 28-01-PLAN.md — Index utilities (load/cache/lookup) + CSS for mode-aware shell +- [ ] 28-02-PLAN.md — Context detection, mode switching, event subscriptions, auto-refresh, mode-aware header **UI hint**: yes ### Phase 29: Per-Paper View @@ -112,7 +115,7 @@ Plans: | 25. Surface Convergence, Doctor & Repair | v1.6 | 3/3 | Complete | 2026-05-04 | | 26. Traceable AI Context Packs | v1.6 | 3/3 | Complete | 2026-05-04 | | 27. Component Library | v1.7 | 2/2 | Complete | 2026-05-04 | -| 28. Dashboard Shell & Context Detection | v1.7 | 0/TBD | Not started | - | +| 28. Dashboard Shell & Context Detection | v1.7 | 2/2 | Planned | - | | 29. Per-Paper View | v1.7 | 0/TBD | Not started | - | | 30. Collection View | v1.7 | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 9dc8add9..fde6c8aa 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,12 +3,13 @@ gsd_state_version: 1.0 milestone: v1.7 milestone_name: Context-Aware Dashboard status: Phase complete — ready for verification -stopped_at: Completed 27-02-PLAN.md +stopped_at: Planned 28-01-PLAN.md + 28-02-PLAN.md last_updated: "2026-05-04T06:42:49.547Z" progress: total_phases: 4 completed_phases: 1 - total_plans: 2 + total_plans: 4 + planned_plans: 2 completed_plans: 2 --- @@ -23,8 +24,8 @@ See: .planning/PROJECT.md (updated 2026-05-03) ## Current Position -Phase: 23 (canonical-asset-index-safe-rebuilds) — EXECUTING -Plan: 3 of 3 +Phase: 28 (dashboard-shell-context-detection) — PLANNING COMPLETE +Plans: 2 of 2 planned ## Performance Metrics diff --git a/.planning/phases/28-dashboard-shell-context-detection/28-01-PLAN.md b/.planning/phases/28-dashboard-shell-context-detection/28-01-PLAN.md new file mode 100644 index 00000000..1866e9f0 --- /dev/null +++ b/.planning/phases/28-dashboard-shell-context-detection/28-01-PLAN.md @@ -0,0 +1,363 @@ +--- +phase: 28-dashboard-shell-context-detection +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - paperforge/plugin/main.js + - paperforge/plugin/styles.css +autonomous: true +requirements: + - DASH-01 # Index loading required for collection mode lookup + - DASH-02 # _findEntry(key) required for per-paper mode + - REFR-01 # _loadIndex() required for index refresh + +must_haves: + truths: + - "PaperForgeStatusView can load and parse formal-library.json via _loadIndex() returning the parsed JSON object or null on failure" + - "PaperForgeStatusView can look up a single paper entry by zotero_key via _findEntry(key) returning the entry or null" + - "PaperForgeStatusView can filter papers by domain name via _filterByDomain(domain) returning an array of matching entries" + - "PaperForgeStatusView has a lazy-loaded cached index via _getCachedIndex() that returns items array after first call" + - "styles.css has a .paperforge-content-area class for the mode-switched content region" + - "styles.css has a .paperforge-mode-context class for the header mode indicator area" + artifacts: + - path: "paperforge/plugin/main.js" + provides: "Index loading utilities: _loadIndex, _getCachedIndex, _findEntry, _filterByDomain" + min_lines: 50 # new JS content for 4 methods + - path: "paperforge/plugin/styles.css" + provides: "CSS for mode-aware header and content layout" + min_lines: 60 # new CSS for header mode context + content area + key_links: + - from: "main.js PaperForgeStatusView._loadIndex" + to: "formal-library.json path" + via: "app.vault.adapter.basePath + plugin.settings.system_dir + 'PaperForge/indexes/formal-library.json'" + - from: "main.js PaperForgeStatusView._findEntry" + to: "_getCachedIndex() items array" + via: "Array.find(item => item.zotero_key === key)" + - from: "main.js PaperForgeStatusView._filterByDomain" + to: "_getCachedIndex() items array" + via: "Array.filter(item => item.domain === domain)" +--- + + +Add index loading utility methods to `PaperForgeStatusView` in `paperforge/plugin/main.js` and add CSS for the mode-aware dashboard layout in `paperforge/plugin/styles.css`. These are the foundational building blocks that Plan 28-02 (Context Detection & Mode Switching) depends on. + +Purpose: Establish data access layer and visual shell so Plan 28-02 can focus purely on context detection logic and mode switching without getting bogged down in index I/O or layout CSS. + +Output: `paperforge/plugin/main.js` with 4 new methods; `paperforge/plugin/styles.css` with 2 new CSS sections. + + + +@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md +@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md + + + +@.planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md +@paperforge/plugin/main.js +@paperforge/plugin/styles.css + + + + + + Task 1: Add index loading utilities (_loadIndex, _getCachedIndex, _findEntry, _filterByDomain) + paperforge/plugin/main.js + + + - paperforge/plugin/main.js — PaperForgeStatusView class (lines 210-815), especially the existing _fetchStats() method at lines 285-375 which already reads formal-library.json with path resolution at lines 294-297 + - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-11, D-12, D-13, D-14, D-16, D-17, D-19) + + + + Add FOUR methods to the `PaperForgeStatusView` class in `paperforge/plugin/main.js`. + + **Insertion point:** Add these methods after the `_buildMetricBar` method (currently around line 399) and before the `_renderStats` method (currently around line 402). This keeps all data-loading utilities grouped together, before the render methods. + + **Method 1: `_loadIndex()` — reads and parses formal-library.json (D-11, D-17, D-19)** + + ```javascript + /* ── Index Loading (D-11, D-17, D-19) ── */ + _loadIndex() { + const vp = this.app.vault.adapter.basePath; + const plugin = this.app.plugins.plugins['paperforge']; + const systemDir = plugin?.settings?.system_dir || '99_System'; + const indexPath = path.join(vp, systemDir, 'PaperForge', 'indexes', 'formal-library.json'); + try { + const raw = fs.readFileSync(indexPath, 'utf-8'); + return JSON.parse(raw); + } catch { + return null; + } + } + ``` + + Uses the same path resolution pattern already established in `_fetchStats()` (lines 294-297 of current main.js). Does NOT cache — always reads from disk. Returns `null` (not an empty object) on failure so callers can distinguish "file missing/corrupt" from "empty index" per D-17. + + **Method 2: `_getCachedIndex()` — lazy-load and cache the items array (D-14)** + + ```javascript + /* ── Cached Index Accessor (D-14) ── */ + _getCachedIndex() { + if (!this._cachedItems) { + const index = this._loadIndex(); + this._cachedItems = index ? (index.items || []) : []; + } + return this._cachedItems; + } + ``` + + - First call loads from disk and caches in `this._cachedItems` + - Subsequent calls return cached array without disk I/O + - Returns empty array (not null) when index is missing + - Cached items must be invalidated by setting `this._cachedItems = null` when a refresh is needed (done in Plan 28-02) + + **Method 3: `_findEntry(key)` — lookup single paper by zotero_key (D-12, D-18)** + + ```javascript + /* ── Single Paper Lookup by Key (D-12, D-18) ── */ + _findEntry(key) { + if (!key) return null; + return this._getCachedIndex().find(item => item.zotero_key === key) || null; + } + ``` + + - Returns the first matching entry, or `null` if not found (per D-18) + - Returns `null` when key is falsy (empty/undefined/null) + + **Method 4: `_filterByDomain(domain)` — filter index items by domain (D-13, D-16)** + + ```javascript + /* ── Filter Papers by Domain (D-13, D-16) ── */ + _filterByDomain(domain) { + if (!domain) return []; + return this._getCachedIndex().filter(item => item.domain === domain); + } + ``` + + - Returns array of matching items + - Returns empty array when domain is falsy + + **IMPORTANT:** Do NOT modify the existing `_fetchStats()` method. It currently has its own inline path resolution — leave it as-is to minimize risk. The new `_loadIndex()` duplicates path resolution intentionally (self-contained, no hidden coupling). + + + + - `main.js` contains a method `_loadIndex()` that reads from `formal-library.json` using path.join with `basePath`, `system_dir`, `PaperForge`, `indexes`, `formal-library.json` + - `_loadIndex()` returns `null` inside the `catch` block (D-17: null means missing/corrupt) + - `main.js` contains a method `_getCachedIndex()` that stores result in `this._cachedItems` + - `_getCachedIndex()` returns `[]` (not null) when `_loadIndex()` returns null + - `main.js` contains a method `_findEntry(key)` using `Array.find(item => item.zotero_key === key)` + - `_findEntry(key)` returns `null` when key is falsy or not found + - `main.js` contains a method `_filterByDomain(domain)` using `Array.filter(item => item.domain === domain)` + - `_filterByDomain(domain)` returns `[]` when domain is falsy + - All 4 methods use proper class method syntax (not arrow functions assigned to `this`) + - grep: `_loadIndex(` exists, `_getCachedIndex(` exists, `_findEntry(` exists, `_filterByDomain(` exists + - grep: `this._cachedItems` appears at least twice (assignment + read) + - grep: `item.zotero_key === key` appears in _findEntry + - grep: `item.domain === domain` appears in _filterByDomain + + + + python -c " +with open('paperforge/plugin/main.js') as f: + c = f.read() +assert '_loadIndex(' in c, 'Missing _loadIndex method' +assert '_getCachedIndex(' in c, 'Missing _getCachedIndex method' +assert '_findEntry(' in c, 'Missing _findEntry method' +assert '_filterByDomain(' in c, 'Missing _filterByDomain method' +assert 'this._cachedItems' in c, 'Missing _cachedItems caching' +assert 'item.zotero_key === key' in c, 'Missing zotero_key lookup' +assert 'item.domain === domain' in c, 'Missing domain filter' +assert 'formal-library.json' in c, 'Missing formal-library.json path reference' +assert 'JSON.parse(raw)' in c, 'Missing JSON parse' +assert 'return null' in c or 'return null;' in c, 'Missing null return on error' +print('Task 1 index utilities: ALL CHECKS PASSED') +" + + + + PaperForgeStatusView has four new methods: _loadIndex() (reads formal-library.json, returns parsed object or null), _getCachedIndex() (lazy-loads and caches items array), _findEntry(key) (lookup by zotero_key, returns entry or null), _filterByDomain(domain) (filters items by domain). All methods handle null/empty inputs gracefully. Cached index invalidatable via this._cachedItems = null. + + + + + Task 2: Add CSS for mode-aware header context and content area layout + paperforge/plugin/styles.css + + + - paperforge/plugin/styles.css (existing file, to append after Section 12 at the end) + - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-06, D-07, D-15) + + + + Append TWO new CSS sections to `paperforge/plugin/styles.css` at the end of the file (after the last line, currently Section 12 — Bar Chart ending at line 1048). + + **Section 13 — Mode-Aware Content Area (D-06)** + + This section provides the container that gets cleared and repopulated on each mode switch: + + ```css + /* ========================================================================== + SECTION 13 — Mode-Aware Content Area + ========================================================================== */ + .paperforge-content-area { + display: flex; + flex-direction: column; + gap: 24px; + transition: opacity 0.2s ease; + } + + .paperforge-content-area.switching { + opacity: 0.4; + pointer-events: none; + } + + .paperforge-content-placeholder { + text-align: center; + padding: 32px 12px; + color: var(--text-muted); + font-size: var(--font-ui-small); + border: 1px dashed var(--background-modifier-border); + border-radius: var(--radius-m); + background: var(--background-secondary); + font-style: italic; + } + ``` + + **Section 14 — Mode-Aware Header Context (D-07, D-15)** + + This section styles the mode context area in the dashboard header that shows the current mode's identifier (paper title, domain name, or "PaperForge"): + + ```css + /* ========================================================================== + SECTION 14 — Mode-Aware Header Context + ========================================================================== */ + .paperforge-mode-context { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; + } + + .paperforge-mode-badge { + font-size: 10px; + font-weight: var(--font-semibold); + padding: 2px 8px; + border-radius: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + line-height: 1.4; + flex-shrink: 0; + } + + .paperforge-mode-badge.global { + background: var(--interactive-accent); + color: var(--text-on-accent); + } + + .paperforge-mode-badge.paper { + background: var(--color-cyan); + color: var(--text-on-accent); + } + + .paperforge-mode-badge.collection { + background: var(--color-purple); + color: var(--text-on-accent); + } + + .paperforge-mode-name { + font-size: var(--font-ui-small); + font-weight: var(--font-semibold); + color: var(--text-normal); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + line-height: 1.3; + } + + .paperforge-mode-warning { + font-size: var(--font-ui-smaller); + color: var(--text-warning); + margin-left: auto; + } + ``` + + **IMPORTANT:** Add BOTH sections at the very end of styles.css, separated by a blank line. Do not modify any existing CSS. The sections must be appended in order (Section 13 first, then Section 14). + + + + - `styles.css` contains a new Section 13 comment `/* ===== SECTION 13 — Mode-Aware Content Area ===== */` + - `styles.css` contains `.paperforge-content-area` with `flex-direction: column` and `gap: 24px` + - `styles.css` contains `.paperforge-content-area.switching` with `opacity: 0.4` and `pointer-events: none` + - `styles.css` contains `.paperforge-content-placeholder` with `border: 1px dashed`, `border-radius: var(--radius-m)`, `font-style: italic` + - `styles.css` contains a new Section 14 comment `/* ===== SECTION 14 — Mode-Aware Header Context ===== */` + - `styles.css` contains `.paperforge-mode-context` with `display: flex`, `align-items: center`, `flex: 1` + - `styles.css` contains `.paperforge-mode-badge.global` with `var(--interactive-accent)` + - `styles.css` contains `.paperforge-mode-badge.paper` with `var(--color-cyan)` + - `styles.css` contains `.paperforge-mode-badge.collection` with `var(--color-purple)` + - `styles.css` contains `.paperforge-mode-name` with `text-overflow: ellipsis` for truncation + - `styles.css` contains `.paperforge-mode-warning` with `var(--text-warning)` + - All colors use `var(--*)` — zero hardcoded hex/rgb values + - File length increases by at least 60 lines (two new sections) + + + + python -c " +with open('paperforge/plugin/styles.css') as f: + c = f.read() +assert 'SECTION 13' in c, 'Missing Section 13 header' +assert '.paperforge-content-area' in c, 'Missing content area class' +assert 'flex-direction: column' in c, 'Missing flex column' +assert 'opacity: 0.4' in c, 'Missing switching opacity' +assert '.paperforge-content-placeholder' in c, 'Missing placeholder class' +assert 'SECTION 14' in c, 'Missing Section 14 header' +assert '.paperforge-mode-context' in c, 'Missing mode context class' +assert '.paperforge-mode-badge.global' in c, 'Missing global badge' +assert '.paperforge-mode-badge.paper' in c, 'Missing paper badge' +assert '.paperforge-mode-badge.collection' in c, 'Missing collection badge' +assert '.paperforge-mode-name' in c, 'Missing mode name class' +assert '.paperforge-mode-warning' in c, 'Missing mode warning class' +assert 'var(--text-warning)' in c, 'Hardcoded warning color' +print('Task 2 CSS mode shell: ALL CHECKS PASSED') +" + + + + styles.css has two new sections: Section 13 (content area container with switching fade transition and placeholder styling) and Section 14 (mode-aware header context with color-coded mode badges, truncated mode name, and warning text). All colors use Obsidian CSS variables. Existing CSS untouched. + + + + + + +1. 4 new index utility methods exist on PaperForgeStatusView: `_loadIndex()`, `_getCachedIndex()`, `_findEntry(key)`, `_filterByDomain(domain)` +2. All 4 methods handle null/empty inputs without throwing +3. `_loadIndex()` returns `null` on file read/parse failure (distinguishable from empty array) +4. `_getCachedIndex()` stores cached items in `this._cachedItems` for invalidation +5. All path resolution uses the same pattern as existing `_fetchStats()`: `basePath + system_dir + 'PaperForge/indexes/formal-library.json'` +6. styles.css has Section 13 and Section 14 appended at end +7. Content area has switching state with opacity fade +8. Placeholder styled as dashed border container for Phase 29/30 content +9. Mode badges have 3 color states: global (accent), paper (cyan), collection (purple) +10. Mode name truncates with `text-overflow: ellipsis` + + + +- [ ] main.js has `_loadIndex()` that reads formal-library.json via the established path pattern, returns parsed JSON or null +- [ ] main.js has `_getCachedIndex()` that lazy-loads and caches items in `this._cachedItems` +- [ ] main.js has `_findEntry(key)` that looks up by zotero_key using Array.find, returns entry or null +- [ ] main.js has `_filterByDomain(domain)` that filters by domain using Array.filter, returns array or [] +- [ ] All 4 methods are class methods on PaperForgeStatusView +- [ ] Existing `_fetchStats()` is untouched (still has its own path resolution) +- [ ] styles.css has Section 13: `.paperforge-content-area` with switching state +- [ ] styles.css has Section 14: `.paperforge-mode-context` with `.paperforge-mode-badge.global/paper/collection`, `.paperforge-mode-name`, `.paperforge-mode-warning` +- [ ] All new CSS uses Obsidian CSS variables only +- [ ] Existing CSS unchanged + + + +After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md` + diff --git a/.planning/phases/28-dashboard-shell-context-detection/28-02-PLAN.md b/.planning/phases/28-dashboard-shell-context-detection/28-02-PLAN.md new file mode 100644 index 00000000..e97d4714 --- /dev/null +++ b/.planning/phases/28-dashboard-shell-context-detection/28-02-PLAN.md @@ -0,0 +1,767 @@ +--- +phase: 28-dashboard-shell-context-detection +plan: 02 +type: execute +wave: 2 +depends_on: + - 28-01 # Index utilities (_loadIndex, _findEntry, _filterByDomain) + mode shell CSS must exist +files_modified: + - paperforge/plugin/main.js + - paperforge/plugin/styles.css +autonomous: true +requirements: + - DASH-01 # .base file -> collection mode + - DASH-02 # .md with zotero_key -> per-paper mode + - DASH-03 # other files / no file -> global mode + - DASH-04 # file switch -> auto-refresh correct mode + - REFR-01 # formal-library.json change -> refresh + - REFR-02 # active file change -> refresh + +must_haves: + truths: + - "Opening a .base file switches dashboard to collection mode with domain name in header" + - "Opening a .md file with zotero_key in frontmatter switches dashboard to per-paper mode with paper title in header" + - "Opening any other file or no file switches dashboard to global mode" + - "Tab click or Ctrl+Tab triggers mode switch within observable time, previous mode data released" + - "External modification of formal-library.json triggers dashboard refresh within 2 seconds, preserving current mode" + - "Index cache invalidates on file modify and file switch events" + - "Quick Actions bar remains visible in all three modes" + artifacts: + - path: "paperforge/plugin/main.js" + provides: "_currentMode, _detectAndSwitch, _switchMode, event subscriptions, mode-aware header render, mode-specific render stubs, onClose cleanup" + min_lines: 150 + - path: "paperforge/plugin/styles.css" + provides: "Contextual action card variants (if any CSS additions needed)" + min_lines: 0 + key_links: + - from: "PaperForgeStatusView.onOpen()" + to: "_detectAndSwitch()" + via: "call at end of onOpen after structural build" + - from: "app.workspace.on('active-leaf-change')" + to: "_detectAndSwitch()" + via: "debounced callback with 300ms delay" + - from: "app.vault.on('modify', file)" + to: "_refreshCurrentMode()" + via: "filter: file.path includes formal-library.json, then invalidate cache + re-detect" + - from: "_detectAndSwitch()" + to: "_switchMode(mode)" + via: "determines mode from active file, then calls _switchMode" + - from: "_switchMode(mode)" + to: "_renderGlobalMode / _renderPaperMode / _renderCollectionMode" + via: "switch statement, each clears _contentEl.empty() then renders" +--- + + +Implement context detection, mode switching, auto-refresh, and mode-aware header rendering in PaperForgeStatusView. The dashboard automatically detects the active file type (`.base`, `.md` with `zotero_key`, or other/no file) and switches to the correct view mode without manual intervention. It also auto-refreshes when the canonical index changes or the user switches files. + +Purpose: Deliver DASH-01 through DASH-04 and REFR-01 through REFR-02. Phase 29 and 30 will flesh out the per-paper and collection views with actual components; this plan provides the routing shell that makes those phases possible. + +Output: `paperforge/plugin/main.js` with mode detection, switching, event subscriptions, and mode-aware rendering. `paperforge/plugin/styles.css` with contextual action variant CSS. + + + +@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md +@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md + + + +@.planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md +@.planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md +@paperforge/plugin/main.js +@paperforge/plugin/styles.css + + + + + + Task 1: Add _currentMode state, _detectAndSwitch(), and _switchMode() infrastructure + paperforge/plugin/main.js + + + - paperforge/plugin/main.js — PaperForgeStatusView class (lines 210-815), especially: + * `onOpen()` at lines 217-225 (must refactor to call _detectAndSwitch) + * `_buildPanel()` at lines 232-280 (must refactor to separate structural header from content) + * `_fetchStats()` at lines 285-375 (will become part of global mode render) + * `_renderOcr()` at lines 432-498 (will become part of global mode render) + - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md + * D-01 through D-07 (context detection and mode switching) + * D-10 (initial data load via _detectAndSwitch) + * D-11 (index loading) + * D-15 (base filename = domain) + * D-17, D-18 (error handling) + * "Specific Ideas" section (contextual actions per mode) + - .planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md (index utilities that now exist) + + + + Refactor `PaperForgeStatusView` to support mode-aware rendering. This is the largest refactor in the phase. + + **=== Step 1: Refactor onOpen() — replace direct content build with mode-detected init ===** + + Replace the existing `onOpen()` method (lines 217-225) with: + + ```javascript + async onOpen() { + this._buildPanel(); + this._contentEl = this.containerEl.querySelector('.paperforge-content-area'); + this._modeSubscribers = []; // reused by both workspace and vault events + this._leafChangeTimer = null; // debounce timer for active-leaf-change + + // Initial data load per D-10 + this._detectAndSwitch(); + } + ``` + + **=== Step 2: Refactor _buildPanel() — separate structural shell from content ===** + + The current `_buildPanel()` (lines 232-280) renders the entire dashboard inline: header, message, metrics, OCR, and quick actions. We need to: + + 1. Keep the header (logo, title, badge, refresh button) in `_buildPanel()` — it stays across modes + 2. Move the metrics (`_metricsEl`) and OCR section (`_ocrSection`) into a NEW structural container that will be swapped per mode + 3. Add a `.paperforge-content-area` div between the header and the Quick Actions section + 4. Keep Quick Actions in `_buildPanel()` — they stay across modes + + Replace the existing `_buildPanel()` method (lines 232-280) with this refactored version: + + ```javascript + _buildPanel() { + const root = this.containerEl; + root.empty(); + root.addClass('paperforge-status-panel'); + + /* ── Header ── */ + const header = root.createEl('div', { cls: 'paperforge-header' }); + + const headerLeft = header.createEl('div', { cls: 'paperforge-header-left' }); + headerLeft.createEl('div', { cls: 'paperforge-header-logo', text: 'P' }); + + // Mode context area (populated by _renderModeHeader) + this._modeContextEl = headerLeft.createEl('div', { cls: 'paperforge-mode-context' }); + + this._headerTitle = headerLeft.createEl('h3', { cls: 'paperforge-header-title', text: 'PaperForge' }); + this._versionBadge = headerLeft.createEl('span', { cls: 'paperforge-header-badge', text: 'v\u2014' }); + + const refreshBtn = header.createEl('button', { cls: 'paperforge-header-refresh', attr: { 'aria-label': 'Refresh' } }); + refreshBtn.innerHTML = '\u21BB'; + refreshBtn.addEventListener('click', () => { + this._invalidateIndex(); + this._detectAndSwitch(); + }); + + /* ── Status Message (command output) ── */ + this._messageEl = root.createEl('div', { cls: 'paperforge-message' }); + + /* ── Mode-Switched Content Area (per D-06) ── */ + this._contentEl = root.createEl('div', { cls: 'paperforge-content-area' }); + + /* ── Quick Actions (visible in all modes per D-07 / "Specific Ideas") ── */ + const actions = root.createEl('div', { cls: 'paperforge-actions-section' }); + actions.createEl('h4', { cls: 'paperforge-actions-title', text: 'Quick Actions' }); + this._actionsGrid = actions.createEl('div', { cls: 'paperforge-actions-grid' }); + this._renderActions(); // extracted so actions can be re-rendered per mode + } + ``` + + Key changes from current: + - Removed `_metricsEl`, `_ocrSection`, `_ocrBadge`, `_ocrTrack`, `_ocrCounts`, `_ocrEmpty` creation from `_buildPanel()` — these will be created dynamically in mode render methods + - Added `_modeContextEl` — populated by `_renderModeHeader()` in Task 2 + - Added `_headerTitle` — reference to update header text per mode + - Added `_contentEl` — the mode-switched content area + - Added `_actionsGrid` — reference to action grid for re-rendering + - Extracted action rendering into `_renderActions()` method + - Refresh button now calls `_invalidateIndex()` + `_detectAndSwitch()` instead of `_fetchStats()` — since mode detection now owns data loading + - Removed the old `_metricsEl`, `_ocrSection` instance variable references from `_buildPanel` — these will be created inside mode render methods instead + + **=== Step 3: Add _renderActions() — extracted from old _buildPanel() ===** + + ```javascript + /* ── Render Quick Actions (extracted from _buildPanel for mode-aware reuse) ── */ + _renderActions() { + this._actionsGrid.empty(); + // Base actions always visible (D-07) + const baseActions = [ + { id: 'paperforge-sync', icon: '\u21BB', title: 'Sync Library', desc: 'Pull new references from Zotero and generate literature notes' }, + { id: 'paperforge-ocr', icon: '\u229E', title: 'Run OCR', desc: 'Extract full text and figures from PDFs via PaddleOCR' }, + { id: 'paperforge-doctor', icon: '\u2695', title: 'Run Doctor', desc: 'Verify PaperForge setup — check configs, Zotero, paths, and index health' }, + { id: 'paperforge-repair', icon: '\u21BA', title: 'Repair Issues', desc: 'Fix three-way state divergence, path errors, and rebuild index' }, + ]; + + for (const a of baseActions) { + const card = this._actionsGrid.createEl('div', { cls: 'paperforge-action-card' }); + card.createEl('div', { cls: 'paperforge-action-card-icon', text: a.icon }); + card.createEl('div', { cls: 'paperforge-action-card-title', text: a.title }); + card.createEl('div', { cls: 'paperforge-action-card-desc', text: a.desc }); + card.createEl('div', { cls: 'paperforge-action-card-hint', text: 'Click to run' }); + const actionDef = ACTIONS.find(x => x.id === a.id); + if (actionDef) { + card.addEventListener('click', () => this._runAction(actionDef, card)); + } + } + } + ``` + + Wait — looking at the existing code, `ACTIONS` is a module-level constant array (lines 157-208). The actions in `_buildPanel` iterate over `ACTIONS` directly. Let me simplify — just use `ACTIONS` directly instead of duplicating action definitions: + + ```javascript + /* ── Render Quick Actions (extracted from _buildPanel for mode-aware reuse) ── */ + _renderActions() { + this._actionsGrid.empty(); + for (const a of ACTIONS) { + const card = this._actionsGrid.createEl('div', { cls: 'paperforge-action-card' }); + card.createEl('div', { cls: 'paperforge-action-card-icon', text: a.icon }); + card.createEl('div', { cls: 'paperforge-action-card-title', text: a.title }); + card.createEl('div', { cls: 'paperforge-action-card-desc', text: a.desc }); + card.createEl('div', { cls: 'paperforge-action-card-hint', text: 'Click to run' }); + card.addEventListener('click', () => this._runAction(a, card)); + } + } + ``` + + Actually this is exactly what the existing code in `_buildPanel()` does (lines 271-279). The refactored `_buildPanel()` calls `_renderActions()` instead. The `ACTIONS` constant is already defined in module scope. Use the exact same iteration as lines 271-279. + + **=== Step 4: Add _invalidateIndex() helper ===** + + ```javascript + /* ── Invalidate cached index (D-14) ── */ + _invalidateIndex() { + this._cachedItems = null; + } + ``` + + **=== Step 5: Add _currentMode state variables ===** + + Add these as instance property initializations. The cleanest approach is to initialize them at the top of the constructor. The current constructor (line 211) is `constructor(leaf) { super(leaf); }`. Replace it with: + + ```javascript + constructor(leaf) { + super(leaf); + this._currentMode = null; // 'global' | 'paper' | 'collection' (D-05) + this._currentDomain = null; // domain name when in collection mode (D-15) + this._currentPaperKey = null; // zotero_key when in per-paper mode (D-03) + this._currentPaperEntry = null; // full entry when in per-paper mode + this._cachedItems = null; // lazy-loaded index items (Plan 28-01) + this._modeSubscribers = []; // event handler refs for cleanup + this._leafChangeTimer = null; // debounce timer for active-leaf-change + } + ``` + + **=== Step 6: Add _detectAndSwitch() — core context detection (D-01, D-02, D-03, D-04, D-10) ===** + + Insert this method after `_renderActions()`: + + ```javascript + /* ── Context Detection & Mode Switch (D-01, D-02, D-03, D-04, D-10) ── */ + _detectAndSwitch() { + const activeFile = this.app.workspace.getActiveFile(); + + if (!activeFile) { + // No active file -> global mode (D-04) + this._switchMode('global'); + return; + } + + const ext = activeFile.extension; + + if (ext === 'base') { + // .base file -> collection mode (D-02, D-15) + this._currentDomain = activeFile.basename; + this._currentPaperKey = null; + this._currentPaperEntry = null; + this._switchMode('collection'); + return; + } + + if (ext === 'md') { + // .md file — check for zotero_key in frontmatter (D-03) + const cache = this.app.metadataCache.getFileCache(activeFile); + const key = cache && cache.frontmatter && cache.frontmatter.zotero_key; + + if (key) { + // Has zotero_key -> per-paper mode (D-03) + this._currentPaperKey = key; + this._currentPaperEntry = this._findEntry(key); + this._currentDomain = null; + this._switchMode('paper'); + } else { + // .md without zotero_key -> global mode (D-04) + this._currentDomain = null; + this._currentPaperKey = null; + this._currentPaperEntry = null; + this._switchMode('global'); + } + return; + } + + // Any other file type -> global mode (D-04) + this._currentDomain = null; + this._currentPaperKey = null; + this._currentPaperEntry = null; + this._switchMode('global'); + } + ``` + + **=== Step 7: Add _switchMode() — renders the correct view for the detected mode (D-05, D-06) ===** + + ```javascript + /* ── Mode Switching (D-05, D-06) ── */ + _switchMode(mode) { + if (this._currentMode === mode) { + // Already in this mode — just refresh if needed (D-04) + this._refreshCurrentMode(); + return; + } + + this._currentMode = mode; + + // Clear existing content (D-06) + this._contentEl.empty(); + this._contentEl.removeClass('switching'); + + // Update header (D-07) + this._renderModeHeader(mode); + + // Render mode-specific content (D-06) + switch (mode) { + case 'global': + this._renderGlobalMode(); + break; + case 'paper': + this._renderPaperMode(); + break; + case 'collection': + this._renderCollectionMode(); + break; + } + } + ``` + + **=== Step 8: Add mode-specific render stubs (placeholder for Phase 29/30) ===** + + These are initial implementations. Phase 29 (Per-Paper View) and Phase 30 (Collection View) will expand these with actual lifecycle, health, maturity, and chart components. + + ```javascript + /* ── Global Mode Render (existing dashboard, extracted from _fetchStats + _renderOcr) ── */ + _renderGlobalMode() { + // Metric cards + this._metricsEl = this._contentEl.createEl('div', { cls: 'paperforge-metrics' }); + const ocrSection = this._contentEl.createEl('div', { cls: 'paperforge-ocr-section' }); + ocrSection.style.display = 'none'; + + // Reuse existing _fetchStats logic — it reads data and renders to _metricsEl + this._cachedStats = null; // force fresh load + this._fetchStats(); + } + + /* ── Per-Paper Mode Render (placeholder — Phase 29 fills this in) ── */ + _renderPaperMode() { + if (this._currentPaperEntry) { + // Entry found — render paper info placeholder (D-18) + this._contentEl.createEl('div', { + cls: 'paperforge-content-placeholder', + text: `Per-paper view for "${this._currentPaperEntry.title || this._currentPaperKey}" — Phase 29 will add lifecycle stepper, health matrix, maturity gauge, and next-step panel here.`, + }); + } else if (this._currentPaperKey) { + // Key exists but entry not found in index (D-18) + this._contentEl.createEl('div', { + cls: 'paperforge-content-placeholder', + text: `Paper "${this._currentPaperKey}" not found in canonical index. The paper may not have been synced yet.`, + }); + } else { + // No key and no entry (defensive fallback) + this._contentEl.createEl('div', { + cls: 'paperforge-content-placeholder', + text: 'No paper data available.', + }); + } + } + + /* ── Collection Mode Render (placeholder — Phase 30 fills this in) ── */ + _renderCollectionMode() { + const domain = this._currentDomain || 'Unknown'; + const domainItems = this._filterByDomain(domain); + + if (domainItems.length > 0) { + this._contentEl.createEl('div', { + cls: 'paperforge-content-placeholder', + text: `Collection view for "${domain}" — ${domainItems.length} papers. Phase 30 will add lifecycle distribution bar chart, aggregated health summary, and paper count here.`, + }); + } else { + this._contentEl.createEl('div', { + cls: 'paperforge-content-placeholder', + text: `No papers found in domain "${domain}". Sync some papers first.`, + }); + } + } + ``` + + **=== Step 9: Add _refreshCurrentMode() — re-renders current mode without switching ===** + + ```javascript + /* ── Refresh current mode (called on index change, D-09, REFR-01) ── */ + _refreshCurrentMode() { + if (!this._currentMode) return; + this._contentEl.empty(); + this._contentEl.addClass('switching'); + this._invalidateIndex(); // force fresh data load + + switch (this._currentMode) { + case 'global': + this._renderGlobalMode(); + break; + case 'paper': + this._renderPaperMode(); + break; + case 'collection': + this._renderCollectionMode(); + break; + } + + // Brief delay before removing switching class for smooth fade transition + setTimeout(() => { + if (this._contentEl) this._contentEl.removeClass('switching'); + }, 50); + } + ``` + + **IMPORTANT — Operation order:** + 1. Replace constructor (add instance vars) + 2. Replace `onOpen()` (use _detectAndSwitch) + 3. Replace `_buildPanel()` (add _modeContextEl, _headerTitle, _contentEl, _actionsGrid; remove _metricsEl, _ocrSection) + 4. Add `_renderActions()` (extracted from old _buildPanel loop) + 5. Add `_invalidateIndex()` + 6. Add `_detectAndSwitch()` + 7. Add `_switchMode()` + 8. Add `_renderGlobalMode()`, `_renderPaperMode()`, `_renderCollectionMode()` + 9. Add `_refreshCurrentMode()` + + Do NOT modify `_fetchStats()`, `_renderStats()`, `_renderOcr()`, or any action-related methods yet. + + + + - `main.js` PaperForgeStatusView constructor initializes: `_currentMode`, `_currentDomain`, `_currentPaperKey`, `_currentPaperEntry`, `_cachedItems`, `_modeSubscribers`, `_leafChangeTimer` + - `onOpen()` calls `this._detectAndSwitch()` and sets up `this._contentEl` + - `_buildPanel()` creates `this._modeContextEl` (div.paperforge-mode-context), `this._headerTitle` (h3.paperforge-header-title), `this._contentEl` (div.paperforge-content-area), `this._actionsGrid` (div.paperforge-actions-grid) + - `_buildPanel()` calls `this._renderActions()` instead of inline rendering + - `_renderActions()` iterates over `ACTIONS` constant and renders action cards + - `_detectAndSwitch()` checks `this.app.workspace.getActiveFile()` (D-01) + - `_detectAndSwitch()` checks `activeFile.extension === 'base'` (D-02) and `activeFile.extension === 'md'` (D-03) + - `_detectAndSwitch()` reads frontmatter via `this.app.metadataCache.getFileCache(activeFile)` (D-03) + - `_detectAndSwitch()` calls `this._switchMode(mode)` with appropriate mode + - `_switchMode()` checks `this._currentMode === mode` to avoid redundant switches (D-04) + - `_switchMode()` calls `this._contentEl.empty()` on mode change (D-06) + - `_switchMode()` calls `this._renderModeHeader(mode)` (D-07) + - `_switchMode()` switch/case handles 'global', 'paper', 'collection' + - `_renderGlobalMode()` creates div.paperforge-metrics and renders _fetchStats() + - `_renderPaperMode()` checks `this._currentPaperEntry` for "not found" case (D-18) + - `_renderCollectionMode()` calls `this._filterByDomain(domain)` for data + - `_refreshCurrentMode()` calls `this._invalidateIndex()` then re-renders current mode + - `_invalidateIndex()` sets `this._cachedItems = null` + - grep: `this._detectAndSwitch()` is called from `onOpen` + - grep: `this._switchMode(` appears in _detectAndSwitch and _switchMode body + - grep: `this._renderModeHeader(` appears in _switchMode body + - grep: `this._renderActions()` appears in _buildPanel + - grep: `this._invalidateIndex()` appears in _refreshCurrentMode and refresh button + - grep: `this._currentMode` appears in _switchMode, _detectAndSwitch, _refreshCurrentMode + + + + python -c " +with open('paperforge/plugin/main.js') as f: + c = f.read() +# Core methods +assert '_detectAndSwitch()' in c or 'this._detectAndSwitch' in c, 'Missing _detectAndSwitch method' +assert '_switchMode(' in c or 'this._switchMode' in c, 'Missing _switchMode method' +assert '_refreshCurrentMode' in c, 'Missing _refreshCurrentMode method' +assert '_invalidateIndex' in c, 'Missing _invalidateIndex method' +assert '_renderActions' in c or '_renderActions()' in c, 'Missing _renderActions method' +assert '_renderGlobalMode' in c, 'Missing _renderGlobalMode method' +assert '_renderPaperMode' in c, 'Missing _renderPaperMode method' +assert '_renderCollectionMode' in c, 'Missing _renderCollectionMode method' +# Mode state +assert '_currentMode' in c, 'Missing _currentMode state' +assert '_currentDomain' in c, 'Missing _currentDomain state' +assert '_currentPaperKey' in c, 'Missing _currentPaperKey state' +assert '_currentPaperEntry' in c, 'Missing _currentPaperEntry state' +# Detection logic +assert \"getActiveFile()\" in c or \"getActiveFile())\" in c, 'Missing getActiveFile call (D-01)' +assert \"'base'\" in c or \"\\\"base\\\"\" in c, 'Missing .base extension check (D-02)' +assert \"getFileCache\" in c, 'Missing getFileCache call (D-03)' +assert \"zotero_key\" in c, 'Missing zotero_key check (D-03)' +# Content management +assert \"empty()\" in c, 'Missing empty() call for content clear (D-06)' +assert '_modeContextEl' in c, 'Missing _modeContextEl (D-07)' +assert '_contentEl' in c, 'Missing _contentEl' +assert '_actionsGrid' in c, 'Missing _actionsGrid' +# Existing structural elements preserved +assert 'paperforge-status-panel' in c, 'Missing root panel class' +assert 'paperforge-header' in c, 'Missing header' +assert 'paperforge-actions-section' in c, 'Missing actions section' +print('Task 1 mode detection infrastructure: ALL CHECKS PASSED') +" + + + + PaperForgeStatusView has mode detection infrastructure: constructor initializes mode state variables, _buildPanel() creates mode-switched content area and header context elements, onOpen() calls _detectAndSwitch(), _renderActions() replaces inline action card loop, _detectAndSwitch() resolves active file type (base/md-with-key/other), _switchMode() clears and renders correct mode, mode-specific render methods (_renderGlobalMode/_renderPaperMode/_renderCollectionMode) exist with content stubs, _refreshCurrentMode() and _invalidateIndex() support auto-refresh workflow. + + + + + Task 2: Add event subscriptions (auto-refresh), mode-aware header rendering, and onClose cleanup + paperforge/plugin/main.js + + + - paperforge/plugin/main.js (output after Task 1 — has the mode infrastructure but no event subscriptions yet) + - .planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md (decisions D-07, D-08, D-09, D-19; also Specific Ideas about contextual actions) + + + + Add event subscriptions, mode-aware header rendering, and lifecycle cleanup to PaperForgeStatusView. + + **WARNING:** There is a duplicate `_showMessage` method issue in the existing code. After the original `_showMessage` at lines 788-793, there's an exact duplicate at lines 795-800, followed by a duplicated `child.on('close', ...)` block (lines 754-786) that was accidentally left in from a previous edit. The executor MUST be careful to insert new code AFTER the first `_showMessage` (ending at line 793) and BEFORE the duplicate block (starting at line 754). To be safe, identify the true end of the class (the `static async open` method at lines 802-814) and insert the new methods immediately before the `static async open` method. + + Insert these methods in the PaperForgeStatusView class, right before the `static async open()` method. + + **Method A: _renderModeHeader(mode) — update header context area (D-07)** + + ```javascript + /* ── Mode-Aware Header (D-07) ── */ + _renderModeHeader(mode) { + this._modeContextEl.empty(); + + // Build mode badge + const badge = this._modeContextEl.createEl('span', { cls: 'paperforge-mode-badge' }); + let modeName = ''; + + switch (mode) { + case 'global': + badge.addClass('global'); + badge.setText('Global'); + this._headerTitle.setText('PaperForge'); + break; + + case 'paper': + badge.addClass('paper'); + badge.setText('Paper'); + if (this._currentPaperEntry && this._currentPaperEntry.title) { + modeName = this._currentPaperEntry.title; + } else if (this._currentPaperKey) { + modeName = this._currentPaperKey; + // Show warning if entry not found (D-18) + this._modeContextEl.createEl('span', { + cls: 'paperforge-mode-warning', + text: 'Not found in index', + }); + } else { + modeName = 'Unknown paper'; + } + this._headerTitle.setText(modeName); + break; + + case 'collection': + badge.addClass('collection'); + badge.setText('Collection'); + modeName = this._currentDomain || 'Unknown Domain'; + this._headerTitle.setText(modeName); + break; + } + + if (modeName) { + this._modeContextEl.createEl('span', { + cls: 'paperforge-mode-name', + text: modeName, + }); + } + } + ``` + + NOTE: For the `paper` case, the modeName text is shown both in the header title AND in the `.paperforge-mode-name` span. This is intentional — the header title is the primary display (large text) and the mode context area shows the supplementary badge + truncated name. The Phase 29 development may adjust this. + + **Method B: _setupEventSubscriptions() — wire up workspace + vault events (D-08, D-09, D-19)** + + ```javascript + /* ── Event Subscriptions (D-08, D-09, D-19) ── */ + _setupEventSubscriptions() { + // D-08: Active leaf change — debounced with 300ms delay + const leafHandler = this.app.workspace.on('active-leaf-change', () => { + clearTimeout(this._leafChangeTimer); + this._leafChangeTimer = setTimeout(() => { + this._detectAndSwitch(); + }, 300); + }); + this._modeSubscribers.push({ event: 'active-leaf-change', ref: leafHandler }); + + // D-09: File modification — filter to formal-library.json only + const modifyHandler = this.app.vault.on('modify', (file) => { + if (file && file.path && file.path.endsWith('formal-library.json')) { + this._invalidateIndex(); // D-14: invalidate cache + this._refreshCurrentMode(); + } + }); + this._modeSubscribers.push({ event: 'modify', ref: modifyHandler }); + } + ``` + + Note on event API: Obsidian's `workspace.on()` and `vault.on()` return an event ref that is passed to `off()` for cleanup. The returned ref may be a number (index) or a function, depending on the Obsidian version. Storing these refs in `this._modeSubscribers` and passing them to `off()` in `onClose()` handles both patterns. + + **Method C: Update onOpen() to call _setupEventSubscriptions()** + + The `onOpen()` method (refactored in Task 1) should also call `_setupEventSubscriptions()`: + + ```javascript + async onOpen() { + this._buildPanel(); + this._contentEl = this.containerEl.querySelector('.paperforge-content-area'); + this._modeSubscribers = []; + this._leafChangeTimer = null; + + // Subscribe to file change events (D-08, D-09) + this._setupEventSubscriptions(); + + // Initial data load per D-10 + this._detectAndSwitch(); + } + ``` + + **Method D: Update onClose() — proper event cleanup with defensive guards** + + ```javascript + onClose() { + // Unsubscribe from all event subscriptions (D-08, D-09) + if (this._modeSubscribers && this._modeSubscribers.length > 0) { + for (const sub of this._modeSubscribers) { + if (sub.event === 'active-leaf-change') { + this.app.workspace.off('active-leaf-change', sub.ref); + } else if (sub.event === 'modify') { + this.app.vault.off('modify', sub.ref); + } + } + this._modeSubscribers = []; + } + + // Clear debounce timer + if (this._leafChangeTimer) { + clearTimeout(this._leafChangeTimer); + this._leafChangeTimer = null; + } + + // Clear cached data + this._cachedItems = null; + this._cachedStats = null; + } + ``` + + **IMPORTANT — Insertion order:** + 1. Replace `onOpen()` with version that calls `_setupEventSubscriptions()` + 2. Replace `onClose()` with cleanup version + 3. Add `_renderModeHeader(mode)` before `static async open()` + 4. Add `_setupEventSubscriptions()` before `static async open()` + + The refactored `onOpen()` from Task 1 will now include the `_setupEventSubscriptions()` call. The executor should make sure both Task 1 and Task 2 changes are present in the final output — specifically, `onOpen()` must call BOTH `_buildPanel()` and `_setupEventSubscriptions()` and `_detectAndSwitch()`. + + The `onClose()` in current main.js (line 227) is a no-op `onClose() { /* no-op */ }`. Replace it completely. + + **No CSS changes** needed for this task. The Task 2 CSS additions from Plan 28-01 already define .paperforge-mode-badge, .paperforge-mode-name, and .paperforge-mode-warning classes. + + + + - `onOpen()` calls `this._setupEventSubscriptions()` in addition to `_buildPanel()` and `_detectAndSwitch()` + - `onClose()` iterates `this._modeSubscribers` and calls `this.app.workspace.off()` and `this.app.vault.off()` for each subscription + - `onClose()` clears `this._leafChangeTimer` and nulls cached data + - `_setupEventSubscriptions()` subscribes to `this.app.workspace.on('active-leaf-change', ...)` (D-08) + - `_setupEventSubscriptions()` subscribes to `this.app.vault.on('modify', ...)` (D-09) + - `modify` handler filters for `file.path.endsWith('formal-library.json')` (D-19) + - `modify` handler calls `this._invalidateIndex()` and `this._refreshCurrentMode()` (D-09, REFR-01) + - `active-leaf-change` handler debounces with `setTimeout(..., 300)` before calling `_detectAndSwitch()` (D-08, D-04) + - Subscriptions are stored in `this._modeSubscribers` array as `{event, ref}` objects + - `_renderModeHeader(mode)` creates `span.paperforge-mode-badge` with `.global/.paper/.collection` class + - `_renderModeHeader(mode)` creates `span.paperforge-mode-name` with mode context text + - `_renderModeHeader('paper')` shows warning when entry not found (D-18) + - `_renderModeHeader('collection')` sets header title to `this._currentDomain` + - `_renderModeHeader('global')` sets header title to 'PaperForge' + - grep: `_setupEventSubscriptions` in main.js + - grep: `'active-leaf-change'` in main.js + - grep: `'modify'` in main.js + - grep: `formal-library.json` in main.js (within event handler filter) + - grep: `this._modeSubscribers.push` in main.js + - grep: `this.app.vault.off(` and `this.app.workspace.off(` in onClose + - grep: `this._leafChangeTimer` and `clearTimeout` in onClose + - grep: `_renderModeHeader` in main.js + - grep: `paperforge-mode-badge` in main.js + - grep: `paperforge-mode-warning` in main.js (paper not found case) + + + + python -c " +with open('paperforge/plugin/main.js') as f: + c = f.read() +# Event subscriptions +assert '_setupEventSubscriptions' in c, 'Missing _setupEventSubscriptions method' +assert \"'active-leaf-change'\" in c, 'Missing active-leaf-change subscription (D-08)' +assert \"'modify'\" in c, 'Missing modify subscription (D-09)' +assert 'formal-library.json' in c, 'Missing formal-library.json filter (D-19)' +assert '_invalidateIndex' in c, 'Missing _invalidateIndex call in modify handler' +assert '_refreshCurrentMode' in c, 'Missing _refreshCurrentMode call' +assert 'this._modeSubscribers.push' in c, 'Missing subscriber tracking' +# Debounce +assert '_leafChangeTimer' in c, 'Missing leaf debounce timer' +assert 'setTimeout' in c, 'Missing setTimeout debounce (D-08)' +# Cleanup +assert 'this.app.workspace.off(\"active-leaf-change\"' in c or 'this.app.workspace.off(' in c, 'Missing workspace off in onClose' +assert 'this.app.vault.off(' in c, 'Missing vault off in onClose' +# Header rendering +assert '_renderModeHeader' in c, 'Missing _renderModeHeader method' +assert \"'paperforge-mode-badge'\" in c, 'Missing mode badge class' +assert \"'paperforge-mode-name'\" in c, 'Missing mode name class' +assert 'addClass(\"global\")' in c, 'Missing global badge class' +assert 'addClass(\"paper\")' in c, 'Missing paper badge class' +assert 'addClass(\"collection\")' in c, 'Missing collection badge class' +assert 'paperforge-mode-warning' in c, 'Missing mode warning (D-18)' +# Lifecycle +assert '_setupEventSubscriptions()' in c or 'this._setupEventSubscriptions' in c, 'onOpen missing setup call' +print('Task 2 events + header + cleanup: ALL CHECKS PASSED') +" + + + + PaperForgeStatusView has full auto-refresh: _setupEventSubscriptions() subscribes to active-leaf-change (debounced 300ms) and vault modify (filtered to formal-library.json), onClose() properly unsubscribes all events, clears timers, and nulls cached data. _renderModeHeader() renders mode-specific badge and context text in the header, with warning for missing papers. + + + + + + +1. PaperForgeStatusView detects active file via getActiveFile() (D-01) and switches to the correct mode: + - .base file -> _switchMode('collection') (DASH-01) + - .md with zotero_key -> _switchMode('paper') (DASH-02) + - other/no file -> _switchMode('global') (DASH-03) +2. File switch via tab click or Ctrl+Tab triggers mode switch within 300ms debounce (DASH-04) +3. formal-library.json modification triggers refresh within 2 seconds (REFR-01) — reactive, no polling +4. Active file change triggers refresh within debounce window (REFR-02) +5. onOpen() calls _detectAndSwitch() for initial load (D-10) +6. _switchMode() clears content via empty() before rendering new mode (D-06) +7. Quick actions (Sync, OCR, Doctor, Repair) remain visible in all modes +8. Mode header shows: global -> "PaperForge", paper -> paper title, collection -> domain name (D-07) +9. Paper not found in index shows mode warning (D-18) +10. onClose() cleans up subscriptions, prevents memory leaks +11. Index cache invalidated on refresh button, file modify event, and onClose + + + +- [ ] .base file -> collection mode with domain name in header (DASH-01) +- [ ] .md with zotero_key -> per-paper mode with paper title in header (DASH-02) +- [ ] Other/no file -> global mode with existing dashboard (DASH-03) +- [ ] File switch triggers mode switch within 300ms (DASH-04) +- [ ] formal-library.json modify triggers refresh, mode preserved (REFR-01) +- [ ] Active leaf change triggers refresh (REFR-02) +- [ ] Quick actions visible in all modes +- [ ] Content cleared on mode switch (empty() + re-render) +- [ ] Mode context displayed in header area (badge + name) +- [ ] Index cache invalidated on modify events +- [ ] Proper event cleanup in onClose() +- [ ] No regressions in existing quick action execution +- [ ] Index not found shows appropriate warning to user + + + +After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-02-SUMMARY.md` +