docs/skills/shade/SKILL.md
Expert help with Shade - the native Swift note capture app. Use for debugging Shade issues, understanding IPC protocols, implementing Hammerspoon integration, nvim RPC, context gathering, and meganote workflows.
npx skillsauth add megalithic/dotfiles shadeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Load the notes skill for nvim-side meganote details:
This skill focuses on Shade app internals: Swift code, IPC, context gathering, nvim RPC, and MLX inference.
Shade is a native Swift floating panel app that hosts a Ghostty terminal running nvim for quick note capture and obsidian.nvim integration. It replaces the previous pure-Hammerspoon approach with a performant, persistent terminal panel.
┌─────────────────────────────────────────────────────────────────┐
│ Hammerspoon │
│ (hotkeys, notifications, sends io.shade.* notifications) │
└──────────────────────────┬──────────────────────────────────────┘
│ DistributedNotificationCenter
▼
┌─────────────────────────────────────────────────────────────────┐
│ Shade.app │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────────────┐ │
│ │ ShadePanel │ │ContextGath-│ │ ShadeNvim │ │
│ │ (floating) │ │ erer │ │ (msgpack-rpc actor) │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ GhosttyKit ││
│ │ (embedded terminal view, nvim shell) ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ ▲ │
│ ▼ │ unix socket │
│ ┌─────────────┐ ┌──────────┴────────────┐ │
│ │ Terminal │ │ ~/.local/state/shade/ │ │
│ │ Surface │ │ nvim.sock │ │
│ └─────────────┘ │ context.json │ │
│ │ shade.pid │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Path | Purpose |
|------|---------|
| ~/code/shade/ | Main Shade Swift source |
| ~/.local/state/shade/ | Runtime state (nvim.sock, context.json, shade.pid) |
| ~/.dotfiles/config/hammerspoon/lib/interop/shade.lua | Hammerspoon integration |
| ~/code/shade/.handoff/ | Integration docs for dotfiles |
Hammerspoon communicates with Shade via DistributedNotificationCenter:
| Notification | Purpose | Shade Action |
|--------------|---------|--------------|
| io.shade.toggle | Toggle panel visibility | Shows/hides panel |
| io.shade.show | Force show panel | Shows panel, activates |
| io.shade.hide | Force hide panel | Hides panel |
| io.shade.quit | Quit Shade | Terminates app |
| io.shade.note.capture | Text capture hotkey | Gathers context, creates capture note |
| io.shade.note.daily | Daily note hotkey | Opens :ObsidianToday via RPC |
| io.shade.note.capture.image | Image capture (clipper) | Reads context.json (imageFilename), creates image note |
| io.shade.mode.sidebar-left | Enter left sidebar mode | Resizes companion window, docks Shade left |
| io.shade.mode.sidebar-right | Enter right sidebar mode | Resizes companion window, docks Shade right |
| io.shade.mode.floating | Return to floating mode | Restores companion window, centers Shade |
// In Hammerspoon (Lua → ObjC bridge):
postNotification("io.shade.note.capture")
// In Shade (ShadeAppDelegate.swift):
@objc func handleCaptureNotification(_ notification: Notification) {
Task {
// 1. Gather context from frontmost app (AX, JXA, nvim RPC)
let context = await ContextGatherer.shared.gather()
// 2. Write context.json for obsidian.nvim template
StateDirectory.writeContext(context)
// 3. Send command to nvim via RPC
try await ShadeNvim.shared.openNewCapture()
// 4. Show panel
showPanel()
}
}
~/.local/state/shade/)| File | Purpose | Format |
|------|---------|--------|
| nvim.sock | Nvim RPC socket | Unix domain socket |
| context.json | Capture context for obsidian.nvim | JSON |
| shade.pid | Process ID for Hammerspoon detection | Plain text |
{
"appType": "browser|terminal|neovim|editor|communication|other",
"appName": "Brave Browser Nightly",
"bundleID": "com.brave.Browser.nightly",
"windowTitle": "GitHub - shade",
"url": "https://github.com/example/shade",
"filePath": "/path/to/file.swift",
"filetype": "swift",
"selection": "selected text here",
"detectedLanguage": "swift",
"line": 42,
"col": 5,
"imageFilename": "20260108-123456.png",
"timestamp": "2026-01-08T12:34:56Z"
}
await ContextGatherer.shared.gather()connect(), openDailyNote(), openNewCapture(), openImageCapture()~/.local/state/shade/nvim.sockwriteContext(), readContext(), readGatheredContext()1. User hits Hyper+Shift+N (Hammerspoon)
2. Hammerspoon posts io.shade.note.capture
3. Shade receives notification
4. ContextGatherer.gather() called:
a. Get frontmost app (NSWorkspace)
b. Detect app type from bundle ID
c. Based on type:
- Browser: JXA for URL/title/selection, fallback to AX
- Terminal: Check for nvim RPC, else AX
- Other: Accessibility API
d. Detect programming language
5. Write context.json
6. Send :Obsidian new_from_template via nvim RPC
7. Show panel
local M = require("lib.interop.shade")
M.isRunning() -- Check if Shade process exists
M.launch(callback) -- Launch Shade.app
M.show() -- Post io.shade.show
M.hide() -- Post io.shade.hide
M.toggle() -- Post io.shade.toggle
M.quit() -- Post io.shade.quit
M.captureWithContext() -- Post io.shade.note.capture (Shade gathers context)
M.openDailyNote() -- Post io.shade.note.daily (Shade handles :ObsidianToday)
M.smartCaptureToggle() -- Toggle or capture based on state
M.smartToggle() -- Smart toggle with auto-launch
-- Sidebar mode functions:
M.sidebarLeft() -- Post io.shade.mode.sidebar-left
M.sidebarRight() -- Post io.shade.mode.sidebar-right
M.floatingMode() -- Post io.shade.mode.floating
M.sidebarToggle() -- Toggle between sidebar-left and floating
M.captureWithContextSidebar() -- Capture note in sidebar mode
Old pattern (deprecated):
-- DON'T DO THIS
sendNvimCommand(":ObsidianToday")
New pattern:
-- DO THIS - Shade handles nvim RPC internally
postNotification(NOTIFICATION_DAILY)
Shade can dock to the left or right of the screen, resizing the "companion" window (the frontmost app when sidebar mode is entered) to share screen space.
io.shade.mode.sidebar-leftio.shade.mode.floating:
The "companion" window is the frontmost app's active window when sidebar mode is entered. Shade:
Shade not working?
│
├─▶ Panel doesn't appear?
│ ├─▶ Check if running: pgrep -x Shade
│ │ ├─▶ NOT running → Launch Shade.app or require("lib.interop.shade").launch()
│ │ └─▶ Running → Check notification delivery (see below)
│ │
│ └─▶ Check notification:
│ └─▶ hs -c "require('lib.interop.shade').toggle()"
│ ├─▶ Works → Hammerspoon hotkey issue
│ └─▶ Doesn't work → Check IPC (notifications)
│
├─▶ nvim commands not executing?
│ ├─▶ Check socket: ls ~/.local/state/shade/nvim.sock
│ │ ├─▶ Missing → nvim not started or wrong path
│ │ └─▶ Exists → Test connection (see below)
│ │
│ └─▶ Test nvim connection:
│ └─▶ nvim --server ~/.local/state/shade/nvim.sock --remote-expr 'v:version'
│ ├─▶ Returns version → ShadeNvim actor issue
│ └─▶ Error → Socket stale or nvim crashed
│
├─▶ Context not captured?
│ ├─▶ Check context.json: cat ~/.local/state/shade/context.json | jq .
│ │ ├─▶ Empty/missing → ContextGatherer not running
│ │ └─▶ Has data → obsidian.nvim template issue
│ │
│ └─▶ Check Accessibility permissions:
│ └─▶ System Preferences → Privacy → Accessibility → Shade
│
└─▶ Image capture not working?
└─▶ Check context.json has imageFilename
├─▶ Missing → Hammerspoon image path not written
└─▶ Present → obsidian.nvim template issue
IPC debugging?
│
├─▶ Hammerspoon → Shade direction:
│ └─▶ 1. Check HS can post: hs -c "require('lib.interop.shade').toggle()"
│ 2. Check Shade logs: log stream --predicate 'subsystem == "io.shade"'
│ 3. Verify notification received in logs
│
├─▶ Shade → nvim direction:
│ └─▶ 1. Check socket: ls ~/.local/state/shade/nvim.sock
│ 2. Test manually: nvim --server ... --remote-expr 'v:version'
│ 3. Check ShadeNvim connection state in logs
│
└─▶ Context flow:
└─▶ 1. Trigger capture
2. Immediately check: cat ~/.local/state/shade/context.json
3. Check if obsidian.nvim reads it
pgrep -x Shade && cat ~/.local/state/shade/shade.pid
ls -la ~/.local/state/shade/nvim.sock
# Test connection:
nvim --server ~/.local/state/shade/nvim.sock --remote-expr 'v:version'
cat ~/.local/state/shade/context.json | jq .
log stream --predicate 'subsystem == "io.shade"' --level debug
# From Hammerspoon console:
require("lib.interop.shade").toggle()
cd ~/code/shade
swift build # Debug build
swift build -c release # Release build
swift run shade # Run debug build
pgrep -x Shadelog stream --predicate 'subsystem == "io.shade"'ls ~/.local/state/shade/nvim.socknvim --server ~/.local/state/shade/nvim.sock --remote-expr 'v:version'~/.local/state/shade/context.json after captureShade uses MLX Swift for native on-device LLM inference. This enables summarization, categorization, and content enrichment without external API calls.
llm (not mlx) for future flexibility┌─────────────────────────────────────────────────────────────────┐
│ Shade.app │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ShadeConfig │ │ MLXInference- │ │ AsyncEnrich- │ │
│ │ (config.json) │ │ Engine (actor) │ │ mentManager │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Capture Pipeline ││
│ │ 1. VisionKit OCR (instant) ││
│ │ 2. Insert OCR text + placeholder ││
│ │ 3. Async: MLX summarize/categorize ││
│ │ 4. nvim RPC: Replace placeholder with enriched content ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
Config is generated by Nix from ~/.dotfiles/home/programs/shade.nix:
# In home-manager config
shadeConfig = {
llm = {
enabled = true;
backend = "mlx";
model = "mlx-community/Qwen3-8B-Instruct-4bit";
preset = "quality"; # quality | balanced | fast
max_tokens = 512;
temperature = 0.7;
};
capture = {
working_directory = config.home.sessionVariables.notes_home + "/captures";
async_enrichment = true;
};
};
xdg.configFile."shade/config.json".text = builtins.toJSON shadeConfig;
Config location: ~/.config/shade/config.json
| Model | Size | Speed | Quality | Use Case | |-------|------|-------|---------|----------| | Qwen3-8B-Instruct-4bit | ~4.3 GB | ~50 tok/s | ★★★★½ | Default (quality) | | Llama-3.2-3B-Instruct-4bit | ~1.8 GB | ~90 tok/s | ★★★½ | Balanced | | Qwen3-4B-Instruct-4bit | ~2.3 GB | ~75 tok/s | ★★★★ | Good balance |
| File | Purpose |
|------|---------|
| Sources/MLXInferenceEngine.swift | Actor for lazy model loading and inference |
| Sources/ShadeConfig.swift | Parse and validate config.json |
| Sources/AsyncEnrichmentManager.swift | Manage background enrichment tasks |
1. User captures image (Hyper+Shift+I)
2. SYNCHRONOUS (instant):
├─ VisionKit extracts OCR text
├─ Create note with OCR text + placeholder:
│ ## Summary
│ <!-- shade:pending:summary -->
└─ Show panel immediately
3. ASYNCHRONOUS (background):
├─ MLXInferenceEngine.summarize(ocrText, context)
├─ When complete, send nvim RPC:
│ nvim_buf_set_lines() to replace placeholder
└─ Optional: nvim notification "✓ Summary ready"
Placeholders mark where async content will be injected:
## Summary
<!-- shade:pending:summary -->
## Tags
<!-- shade:pending:tags -->
After enrichment:
## Summary
This image shows a code snippet implementing...
## Tags
#code #swift #mlx
# Check if config is valid
cat ~/.config/shade/config.json | jq .
# Check model cache location
ls ~/Library/Caches/mlx-swift/
# View LLM-specific logs
log stream --predicate 'subsystem == "io.shade" AND category == "llm"' --level debug
# Test MLX inference directly (if CLI available)
swift run shade --test-llm "Summarize: Hello world"
LLM not working?
│
├─▶ Model not loading?
│ ├─▶ Check config: cat ~/.config/shade/config.json | jq .llm
│ ├─▶ Check model exists: ls ~/Library/Caches/mlx-swift/
│ └─▶ Check memory: Model needs ~8GB for Qwen3-8B
│
├─▶ Enrichment not appearing?
│ ├─▶ Check async_enrichment enabled in config
│ ├─▶ Check placeholder exists in note
│ └─▶ Check nvim RPC connection (see nvim debugging above)
│
└─▶ Quality issues?
├─▶ Try larger model (more quality, more memory)
├─▶ Adjust temperature (lower = more deterministic)
└─▶ Check prompt templates in obsidian.nvim config
shade-qji: Context Gathering in Shade (completed Jan 2026)
shade-ahf: MLX Swift Integration (in progress Jan 2026)
testing
Apply Strunk's timeless writing rules to ANY prose humans will read - documentation, commit messages, error messages, explanations, reports, or UI text. Makes your writing clearer, stronger, and more professional.
tools
Web search using DuckDuckGo (free, unlimited). Falls back to pi-web-access extension for content extraction.
tools
Interact with web pages using agent-browser CLI. MUST run 'browser connect 9222' FIRST to use existing browser with authenticated sessions.
tools
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.