init: Obsidian Parallel Reader v0.1.0

Split-view reading plugin for long-form notes:
- LLM self-adaptive segmentation (no heading dependency)
- Anchor-based line resolution with fallbacks
- Scroll-sync highlight + click-to-jump
- Right-click context menu for copy actions
- MarkdownRenderer for native table/bold/code rendering
- SHA-1 content-hash cache with stale-detection banner
- Three backends: Claude Code CLI / Codex CLI / Anthropic API

Change-Id: I105c57e3df0b2d8d1570c31e3e3a689ecf52ff71
This commit is contained in:
wujunchen 2026-04-24 19:31:23 +08:00
commit ff856f719b
6 changed files with 1357 additions and 0 deletions

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Plugin runtime data (user cache + settings, not source)
data.json
# Editor / OS
.DS_Store
.vscode/
.idea/
*.swp
*.swo
# Node (if build tooling is added later)
node_modules/
dist/
*.log

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 wujunchen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

104
README.md Normal file
View file

@ -0,0 +1,104 @@
# Obsidian Parallel Reader
An Obsidian plugin that splits any long note into a left/right parallel-reading layout: the original content stays on the left; LLM-generated section summaries appear on the right. Scrolling the source highlights the matching summary card; clicking a summary jumps the source editor.
Inspired by the reading workflow demo in [this Bilibili video](https://www.bilibili.com/video/BV1FxoGBVETm/).
## Features
- **Self-adaptive segmentation** — the LLM decides natural topic boundaries, no need to match markdown headings. Short adjacent sections merge; long ones split.
- **Anchor-based line resolution** — summaries carry a verbatim quote from the source; the plugin finds the line number by `indexOf` (with graceful fallbacks).
- **Scroll-sync highlighting** — scrolling the source editor updates the active card in the summary pane.
- **Right-click → copy** — Markdown / plain text / anchor quote / jump to source, as a native Obsidian context menu.
- **Markdown rendering** — summaries render through Obsidian's official `MarkdownRenderer`, so tables, bold, code, wikilinks all work.
- **Persistent cache** — generated summaries are cached by SHA-1 of source content. Reopening a note shows the cached view instantly; edits invalidate the cache (shows a stale banner).
- **Three backends** — Claude Code CLI, Codex CLI, or Anthropic API. CLI backends reuse your existing subscription and don't need a separate API key.
## Installation
### Manual install (until this lands in Community Plugins)
1. Clone this repo into `.obsidian/plugins/parallel-reader/` in your vault, or symlink it there:
```bash
ln -s /path/to/obsidian-parallel-reader /path/to/YourVault/.obsidian/plugins/parallel-reader
```
2. In Obsidian: **Settings → Community plugins → Installed plugins** → enable **Parallel Reader**.
### Pick a backend
| Backend | Auth | Cost | Notes |
|---------|------|------|-------|
| **Claude Code CLI** | OAuth in macOS Keychain | Included in Claude subscription | ⚠️ Keychain ACL may block Obsidian subprocess reads — may not work from GUI |
| **Codex CLI** | File-based token (`~/.codex/auth.json`) | Included in ChatGPT Plus | ✅ Works from Obsidian GUI; recommended |
| **Anthropic API (direct)** | API key in plugin settings | Separate billing | Fallback when CLI auth fails |
### Configure the CLI path
Obsidian launched from Finder does **not** inherit your shell `PATH`. Create a stable symlink in `~/bin/` and point the plugin setting at it:
```bash
mkdir -p ~/bin
# Claude Code (Mach-O binary, no Node needed)
ln -sf "$(readlink -f "$(which claude)")" ~/bin/claude
# Codex (Node script — use the wrapper pattern below instead)
cat > ~/bin/codex <<'EOF'
#!/bin/bash
NODE="/path/to/your/node"
CODEX_JS="/path/to/@openai/codex/bin/codex.js"
exec "$NODE" "$CODEX_JS" "$@"
EOF
chmod +x ~/bin/codex
```
Then in the plugin settings, paste `/Users/you/bin/codex` (or `~/bin/claude`) into **CLI 路径**, and click **Test** to verify.
## Commands
| Command | What it does |
|---------|--------------|
| `为当前笔记生成对照笔记(缓存优先)` | Show cached summary if present, otherwise call LLM |
| `强制重新生成(绕过缓存)` | Re-run the LLM even if cache matches |
| `打开对照笔记面板` | Open right-pane view (loads cache if present) |
| `清除当前笔记的缓存` | Drop the cache entry for the active note |
| `清除所有缓存` | Wipe all cached summaries |
## Interaction
| Action | Effect |
|--------|--------|
| Left-click a card body | Scroll source editor to that section |
| Right-click a card | Context menu: Copy Markdown / Copy plain text / Copy anchor / Jump |
| Drag to select text | Normal text selection (does not trigger jump) |
| Scroll source editor | Active card gets highlighted on the right |
## Design notes
The LLM is asked to return JSON with this shape:
```json
{
"cards": [
{
"title": "3-10 character short heading",
"anchor": "40-80 characters, verbatim from source, used for line resolution",
"gist": "20-40 character one-line summary serving as a lead-in",
"bullets": ["3-6 supporting details, 20-50 chars each"]
}
]
}
```
- The **anchor** is the key mechanism that keeps scroll-sync working without relying on markdown headings. It's copied verbatim so `content.indexOf(anchor)` finds the origin line; graceful fallbacks (prefix trimming, whitespace normalization) handle minor LLM drift.
- The **gist + bullets** dual structure was chosen after several iterations — pure prose felt wall-of-text, pure bullets felt fragmented. A one-line lead-in before bullets gives both overview and scannable detail.
- Output is rendered via `MarkdownRenderer.render()` so any tables / bold / code / wikilinks in the LLM response render natively.
## Known limitations
- **Document size cap**: 20,000 characters. Longer notes are truncated.
- **Claude Code CLI auth**: Obsidian subprocess reads of the macOS Keychain `Claude Code-credentials` service may fail due to code-signing ACL. Codex backend doesn't have this issue.
- **No streaming**: generation is a single batch call; expect ~5-15s wait depending on document length and backend.
- **Preview mode**: scroll-sync uses Obsidian's `setEphemeralState` which works in reading mode too, but accuracy depends on how CodeMirror resolves line positions.
## License
MIT

1011
main.js Normal file

File diff suppressed because it is too large Load diff

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"id": "parallel-reader",
"name": "Parallel Reader",
"version": "0.1.0",
"minAppVersion": "1.4.0",
"description": "Split-view reading: left = original note, right = LLM-generated section bullets with scroll-sync highlight.",
"author": "lancivez",
"isDesktopOnly": true
}

198
styles.css Normal file
View file

@ -0,0 +1,198 @@
.parallel-reader-container {
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.parallel-reader-header {
padding: 12px 14px 8px;
border-bottom: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
flex-shrink: 0;
}
.parallel-reader-title {
font-weight: 600;
font-size: 13px;
color: var(--text-normal);
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.parallel-reader-cards {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.parallel-reader-card {
padding: 16px 18px;
margin-bottom: 14px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-left: 3px solid var(--background-modifier-border);
border-radius: 6px;
cursor: pointer;
transition: border-left-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
user-select: text;
-webkit-user-select: text;
}
.parallel-reader-card * {
user-select: text;
-webkit-user-select: text;
}
.parallel-reader-card:hover {
background: var(--background-secondary);
border-left-color: var(--interactive-accent);
}
.parallel-reader-card.is-active {
border-left-color: var(--interactive-accent);
border-left-width: 4px;
background: var(--background-secondary);
box-shadow: 0 0 0 1px var(--interactive-accent-hover, var(--interactive-accent));
}
.parallel-reader-card-title {
font-weight: 700;
font-size: 15px;
color: var(--text-accent, var(--interactive-accent));
margin-bottom: 10px;
line-height: 1.45;
}
.parallel-reader-gist {
font-size: 14px;
color: var(--text-normal);
line-height: 1.75;
margin-bottom: 12px;
font-weight: 500;
}
.parallel-reader-gist p:first-child { margin-top: 0; }
.parallel-reader-gist p:last-child { margin-bottom: 0; }
.parallel-reader-bullets-md {
font-size: 13px;
color: var(--text-muted);
line-height: 1.75;
}
.parallel-reader-bullets-md ul {
margin: 0;
padding-left: 22px;
}
.parallel-reader-bullets-md li {
margin-bottom: 8px;
}
.parallel-reader-bullets-md li:last-child {
margin-bottom: 0;
}
.parallel-reader-bullets-md li::marker {
color: var(--interactive-accent);
}
.parallel-reader-bullets-md strong {
color: var(--text-normal);
}
.parallel-reader-bullets-md code {
font-size: 12px;
background: var(--code-background, var(--background-secondary));
padding: 1px 4px;
border-radius: 3px;
}
.parallel-reader-bullets-md table {
font-size: 12px;
border-collapse: collapse;
margin: 6px 0;
width: 100%;
}
.parallel-reader-bullets-md th {
background: var(--background-secondary);
font-weight: 600;
color: var(--text-normal);
}
.parallel-reader-bullets-md th,
.parallel-reader-bullets-md td {
border: 1px solid var(--background-modifier-border);
padding: 4px 8px;
text-align: left;
}
.parallel-reader-lvl {
color: var(--text-muted);
font-family: var(--font-monospace);
font-size: 11px;
margin-right: 2px;
}
.parallel-reader-card-unanchored {
opacity: 0.75;
}
.parallel-reader-card-unanchored:hover {
opacity: 1;
}
.parallel-reader-warn {
color: var(--text-warning, #e0af68);
font-size: 11px;
margin-left: 4px;
cursor: help;
}
.parallel-reader-bullets {
margin: 0;
padding-left: 18px;
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
}
.parallel-reader-bullets li {
margin-bottom: 3px;
}
.parallel-reader-empty-li {
font-style: italic;
color: var(--text-faint);
}
.parallel-reader-stale-banner {
margin: 10px 12px 0;
padding: 8px 12px;
background: var(--background-modifier-error, rgba(224, 175, 104, 0.1));
color: var(--text-warning, #e0af68);
border-radius: 4px;
font-size: 12px;
border: 1px solid var(--text-warning, #e0af68);
}
.parallel-reader-empty {
padding: 20px;
text-align: center;
color: var(--text-muted);
}
.parallel-reader-empty code {
display: inline-block;
margin-top: 8px;
padding: 4px 8px;
background: var(--background-secondary);
border-radius: 3px;
font-size: 11px;
}