mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
docs(28): create phase 28 plans (index utilities + context detection shell)
This commit is contained in:
parent
3776cb3693
commit
0fb7eaaccf
4 changed files with 1145 additions and 11 deletions
|
|
@ -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 | - |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
|
||||
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/28-dashboard-shell-context-detection/28-CONTEXT.md
|
||||
@paperforge/plugin/main.js
|
||||
@paperforge/plugin/styles.css
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add index loading utilities (_loadIndex, _getCachedIndex, _findEntry, _filterByDomain)</name>
|
||||
<files>paperforge/plugin/main.js</files>
|
||||
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
|
||||
<verify>
|
||||
<automated>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')
|
||||
"</automated>
|
||||
</verify>
|
||||
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add CSS for mode-aware header context and content area layout</name>
|
||||
<files>paperforge/plugin/styles.css</files>
|
||||
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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)
|
||||
</acceptance_criteria>
|
||||
|
||||
<verify>
|
||||
<automated>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')
|
||||
"</automated>
|
||||
</verify>
|
||||
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-01-SUMMARY.md`
|
||||
</output>
|
||||
|
|
@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
|
||||
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add _currentMode state, _detectAndSwitch(), and _switchMode() infrastructure</name>
|
||||
<files>paperforge/plugin/main.js</files>
|
||||
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
|
||||
<verify>
|
||||
<automated>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')
|
||||
"</automated>
|
||||
</verify>
|
||||
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add event subscriptions (auto-refresh), mode-aware header rendering, and onClose cleanup</name>
|
||||
<files>paperforge/plugin/main.js</files>
|
||||
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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)
|
||||
</acceptance_criteria>
|
||||
|
||||
<verify>
|
||||
<automated>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')
|
||||
"</automated>
|
||||
</verify>
|
||||
|
||||
<done>
|
||||
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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] .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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/28-dashboard-shell-context-detection/28-02-SUMMARY.md`
|
||||
</output>
|
||||
Loading…
Reference in a new issue