---
phase: 26-traceable-ai-context-packs
plan: 03
type: execute
wave: 2
depends_on:
  - "26-02"
files_modified:
  - paperforge/plugin/main.js
autonomous: true
requirements:
  - AIC-02
  - AIC-03
  - AIC-04

must_haves:
  truths:
    - "User can click a 'Copy Context' button from any paper note to copy that paper's canonical index entry JSON to clipboard"
    - "User can click a 'Copy Collection Context' button from the PaperForge dashboard to copy filtered context JSON to clipboard"
    - "Copied JSON is identical to what paperforge context <key> would output"
    - "If a paper cannot provide AI context, the copied output explains why"
    - "The 'Copy Context' action is available from the command palette"
  artifacts:
    - path: paperforge/plugin/main.js
      provides: "Two new Quick Actions: 'Copy Context' (per-paper) and 'Copy Collection Context' (multi-paper)"
  key_links:
    - from: paperforge/plugin/main.js ACTIONS entry
      to: paperforge CLI context command
      via: "spawn('python', ['-m', 'paperforge', 'context', key], ...) in _runAction"
      pattern: "context|Copy Context"
---

<objective>
Add "Copy Context" and "Copy Collection Context" buttons to the Obsidian plugin, providing GUI access to the `paperforge context` CLI command. These are the user-facing entry points for AI context packs (AIC-02, AIC-03, AIC-04).

Purpose: Users interact with PaperForge primarily through the Obsidian plugin. CLI-only context generation would force users to switch to terminal. Plugin buttons make context generation a one-click operation from the dashboard or from any paper note.

Output:
- `paperforge/plugin/main.js` — Two new ACTIONS entries, key-resolution logic, clipboard output
</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/STATE.md
@.planning/ROADMAP.md
@.planning/phases/26-traceable-ai-context-packs/26-CONTEXT.md

<interfaces>
Plugin ACTIONS array pattern (paperforge/plugin/main.js lines 157-190):
```javascript
const ACTIONS = [
    {
        id: 'paperforge-sync',
        title: 'Sync Library',
        desc: 'Pull new references from Zotero and generate literature notes',
        icon: '\u21BB',  // ↻
        cmd: 'sync',
        okMsg: 'Sync complete',
    },
    // ...more actions...
];
```

Plugin action execution (main.js _runAction, lines 446-493):
```javascript
_runAction(a, card) {
    card.addClass('running');
    const vp = this.app.vault.adapter.basePath;
    this._showMessage('Processing...', 'running');
    const { spawn } = require('node:child_process');
    const child = spawn('python', ['-m', 'paperforge', a.cmd], { cwd: vp, timeout: 600000 });
    // stdout/stderr handlers, close handler with clipboard copy
    child.on('close', (code) => {
        if (code === 0) {
            // Copy output to clipboard
            const output = log.join('\n');
            navigator.clipboard.writeText(output);
            new Notice('[OK] Context copied to clipboard');
        }
    });
}
```

The current `_runAction` uses `a.cmd` directly as the subcommand. For context actions, the command needs additional arguments (zotero key or filter flags).

CLI context command (from Plan 26-02):
- `python -m paperforge context <key>` — single paper
- `python -m paperforge context --domain <domain>` — domain filter
- `python -m paperforge context --collection <path>` — collection filter
- `python -m paperforge context --all` — all entries

Decisions from CONTEXT.md:
- D-07: Plugin adds "Copy Context" button for the current paper (copies canonical index entry JSON).
- D-08: Plugin adds "Copy Collection Context" button (from Base view or dashboard, copies filtered entries).
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
<name>Task 1: Add "Copy Context" Quick Action with active-paper key resolution</name>
<files>
  paperforge/plugin/main.js
</files>

<read_first>
  paperforge/plugin/main.js:157-190 (ACTIONS array definition)
  paperforge/plugin/main.js:446-493 (_runAction implementation)
</read_first>

<action>
Add a "Copy Context" action to the ACTIONS array and update `_runAction` to support key-specific commands.

**Change A — Extend ACTIONS array:**
Add a new entry to ACTIONS:
```javascript
{
    id: 'paperforge-copy-context',
    title: 'Copy Context',
    desc: 'Copy this paper\u2019s canonical index entry JSON to clipboard for AI use',
    icon: '\u2139',  // ℹ
    cmd: 'context',  // CLI subcommand
    needsKey: true,  // NEW: signals this action needs the current paper's zotero_key
    okMsg: 'Context copied',
},
```

The `needsKey: true` flag is new. Other actions (sync, ocr, doctor, repair) set `needsKey: false` or omit it.

**Change B — Update `_runAction` to handle key-specific commands and clipboard copy:**

Modify `_runAction(a, card)` to:

1. Resolve the zotero key when `a.needsKey` is true:
   ```javascript
   let extraArgs = [];
   if (a.needsKey) {
       // Try to get the active file's zotero_key from its frontmatter
       const activeFile = this.app.workspace.getActiveFile();
       let key = null;
       if (activeFile) {
           const cache = this.app.metadataCache.getFileCache(activeFile);
           if (cache && cache.frontmatter && cache.frontmatter.zotero_key) {
               key = cache.frontmatter.zotero_key;
           }
       }
       if (!key) {
           this._showMessage('[!!] No zotero_key found in active note frontmatter', 'error');
           new Notice('[!!] Open a paper note with a zotero_key in its frontmatter first', 6000);
           card.removeClass('running');
           return;
       }
       extraArgs = [key];
   }
   ```

2. Build the spawn command with extra args:
   ```javascript
   const child = spawn('python', ['-m', 'paperforge', a.cmd, ...extraArgs], { cwd: vp, timeout: 30000 });
   ```

3. On successful completion (close code === 0), copy output to clipboard:
   ```javascript
   child.on('close', (code) => {
       clearInterval(pollTimer);
       card.removeClass('running');
       const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
       if (code !== 0) {
           const last = log.slice(-3).join(' | ') || 'exit code ' + code;
           this._showMessage('[!!] ' + last, 'error');
           new Notice('[!!] ' + a.cmd + ' failed: ' + last, 8000);
       } else {
           // Copy all logged output to clipboard
           const output = log.join('\n');
           if (output.trim()) {
               try {
                   // Validate JSON before copying
                   JSON.parse(output);
                   navigator.clipboard.writeText(output).then(() => {
                       const summary = `${elapsed}s \u2014 ${output.length} chars copied`;
                       this._showMessage('[OK] ' + a.title + ': ' + summary, 'ok');
                       new Notice('[OK] ' + a.okMsg + ' \u2014 ' + output.length + ' chars');
                   }).catch((err) => {
                       this._showMessage('[!!] Clipboard write failed: ' + err.message, 'error');
                       new Notice('[!!] Clipboard error', 6000);
                   });
               } catch (e) {
                   this._showMessage('[!!] Invalid JSON from context command', 'error');
                   new Notice('[!!] Context command returned invalid JSON', 8000);
               }
           } else {
               this._showMessage('[!!] No output from context command', 'error');
               new Notice('[!!] Context command returned empty output', 8000);
           }
           this._fetchStats(true);
       }
   });
   ```

4. For existing actions (sync, ocr, doctor, repair) that don't set `needsKey`, keep existing behavior: `extraArgs` stays empty, timeout stays at 600000.

**Change C — Add command palette registration for "Copy Context":**

After the existing commands block (around line 1086), register a command palette entry:
```javascript
this.addCommand({
    id: 'paperforge-copy-context',
    name: 'Copy Context: Copy active paper\u2019s index entry JSON to clipboard',
    callback: () => {
        // Find the copy-context action and trigger it
        const action = ACTIONS.find(a => a.id === 'paperforge-copy-context');
        if (action) {
            // Create a temporary card element for the loading UI
            const tempCard = createEl('div');
            this._runAction(action, tempCard);
        }
    },
});
```

This command palette entry is registered inside `onload()` (the plugin's main initialization function). Find the `onload()` method and add the `this.addCommand(...)` call there.

Keep all existing ACTIONS entries unchanged.
</action>

<acceptance_criteria>
- ACTIONS array has new entry with `id: 'paperforge-copy-context'`, `needsKey: true`, `cmd: 'context'`
- `_runAction()` checks `a.needsKey` and resolves zotero_key from active file frontmatter
- When zotero_key is missing, shows error notice and returns early
- On successful context output, copies JSON to clipboard via `navigator.clipboard.writeText()`
- Validates JSON before copying (prevents clipboard pollution)
- Command palette entry registered: `Copy Context: Copy active paper's index entry JSON to clipboard`
</acceptance_criteria>

<verify>
  <automated>MISSING — Wave 0 must create test scaffolding first. Verify manually: open Obsidian, open a paper note with zotero_key frontmatter, click "Copy Context" action, paste clipboard content to verify valid JSON.</automated>
</verify>

<done>
"Copy Context" button added to plugin dashboard. Reads zotero_key from active note, calls `paperforge context <key>`, copies JSON output to clipboard. Command palette entry also available.
</done>
</task>

<task type="auto" tdd="false">
<name>Task 2: Add "Copy Collection Context" Quick Action for multi-paper context</name>
<files>
  paperforge/plugin/main.js
</files>

<read_first>
  paperforge/plugin/main.js (ACTIONS array, _runAction, _fetchStats method)
</read_first>

<action>
Add a "Copy Collection Context" action that copies context for all papers in the current view or filter.

**Change A — Add to ACTIONS array:**
```javascript
{
    id: 'paperforge-copy-collection-context',
    title: 'Copy Collection Context',
    desc: 'Copy canonical index entries for all visible papers to clipboard',
    icon: '\u2261',  // ≡
    cmd: 'context',
    needsFilter: true,  // NEW: signals this action needs a filter flag
    okMsg: 'Collection context copied',
},
```

The `needsFilter: true` flag is new. It means this action should add a filter argument to the CLI command (e.g. `--all`, `--domain`, or `--collection`).

**Change B — Update `_runAction` filter resolution:**

Add filter resolution logic after the key resolution block:
```javascript
if (a.needsKey) {
    // ... existing key resolution from Task 1 ...
}
if (a.needsFilter) {
    // Default to --all if no specific filter can be determined
    let filterArg = '--all';
    
    // Phase 25 dashboard stores domain stats; try to read current domain filter
    // For now, default to --all (the agent's discretion, per "the agent's Discretion" in CONTEXT.md)
    // Future enhancement: read active Base view filter
    
    extraArgs = [filterArg];
}
```

**Change C — Handle collection context output in close handler:**

The close handler already handles clipboard copy from Task 1. The collection context output is a JSON array (since `--all` / `--domain` / `--collection` output is always an array per D-06). The existing clipboard logic in Task 1's close handler already handles this — no additional changes needed for the output handling.

**Key design decisions:**
- Default to `--all` for the initial implementation (per "the agent's Discretion" in CONTEXT.md about plugin button placement)
- The output is a JSON array, which may be large. The clipboard `writeText` API handles up to ~1MB on most platforms. For very large libraries, users may need to use the CLI directly.
- The dashboard button triggers on click from any paper. The collection context represents all papers in the canonical index (or the current filtered view in a future enhancement).

**Change D — Add command palette entry:**

After the "Copy Context" command registration, add:
```javascript
this.addCommand({
    id: 'paperforge-copy-collection-context',
    name: 'Copy Collection Context: Copy all canonical index entries to clipboard',
    callback: () => {
        const action = ACTIONS.find(a => a.id === 'paperforge-copy-collection-context');
        if (action) {
            const tempCard = createEl('div');
            this._runAction(action, tempCard);
        }
    },
});
```

Add both command palette entries inside the plugin's `onload()` method. Look for the existing command palette entries (there should be one like `paperforge: Open Dashboard` at around line 1125):
```javascript
// Find existing command registration block and add after it:
this.addCommand({
    id: 'paperforge-copy-context',
    name: 'Copy Context: Copy active paper...',
    callback: () => { ... }
});
this.addCommand({
    id: 'paperforge-copy-collection-context',  
    name: 'Copy Collection Context: Copy all canonical...',
    callback: () => { ... }
});
```
</action>

<acceptance_criteria>
- ACTIONS array has entry with `id: 'paperforge-copy-collection-context'`, `needsFilter: true`
- `_runAction()` adds `--all` filter argument when `a.needsFilter` is true
- Dashboard shows the "Copy Collection Context" card in Quick Actions grid
- Command palette has entry for "Copy Collection Context"
</acceptance_criteria>

<verify>
  <automated>MISSING — Requires manual Obsidian testing. Open plugin dashboard, verify "Copy Collection Context" card appears in Quick Actions grid. Click it, paste clipboard content to see JSON array of all entries.</automated>
</verify>

<done>
"Copy Collection Context" button added to plugin dashboard. Copies JSON array of all canonical index entries to clipboard. Command palette entry registered.
</done>
</task>

<task type="auto" tdd="false">
<name>Task 3: Error handling, edge cases, and integration robustness</name>
<files>
  paperforge/plugin/main.js
</files>

<read_first>
  paperforge/plugin/main.js (full _runAction, the ACTIONS array, Dashboard onOpen)
</read_first>

<action>
Add error handling and edge case coverage for the two new actions:

**Change A — Zotero key not found in active note:**
Already implemented in Task 1 (shows error notice). Verify the error flow:
- When `needsKey` is true and `app.workspace.getActiveFile()` is null (no note open): show error
- When `needsKey` is true and frontmatter exists but has no `zotero_key`: show error mentioning the missing property
- When `needsKey` is true and there's no frontmatter at all: show error

**Change B — Context command returns empty or invalid JSON:**
Already implemented in Task 1 (JSON.parse validation). Ensure the error message is specific: "Context command returned invalid JSON" rather than a generic error.

**Change C — Large collection context timeout:**
The collection context (--all) may be large for vaults with many papers. Increase timeout for `needsFilter` actions:
```javascript
const cmdTimeout = a.needsFilter ? 60000 : (a.needsKey ? 30000 : 600000);
const child = spawn('python', ['-m', 'paperforge', a.cmd, ...extraArgs], { cwd: vp, timeout: cmdTimeout });
```

**Change D — Loading state for new actions:**
Ensure the action card's "running" state works properly:
- The `card.addClass('running')` and `card.removeClass('running')` calls in `_runAction` work for dashboard cards
- For command palette invocations (which pass a temporary card element), removing the class is a no-op since the element is never added to DOM. This is acceptable — the notice provides feedback.

**Change E — Multiple rapid clicks:**
Add a guard to prevent running the same action simultaneously:
```javascript
if (card.classList.contains('running')) {
    return;  // Already running
}
```
Add this at the very beginning of `_runAction`, before any other logic.

**Change F — Close handler JSON validation robustness:**
Replace the simple `JSON.parse(output)` with a try-catch:
```javascript
try {
    JSON.parse(output);
} catch (e) {
    this._showMessage('[!!] Invalid JSON from ' + a.title, 'error');
    new Notice('[!!] ' + a.title + ' returned invalid JSON: ' + e.message.slice(0, 100), 8000);
    return;
}
```

Wrap these error-handling additions inside the existing `_runAction` method without disrupting the existing close handler logic for sync/ocr/doctor/repair actions. The existing `log` accumulation, polling, and elapsed-time display must remain unchanged for those actions.

**Change G — Verify ACTIONS integration with command palette:**
All ACTIONS entries should still render in the dashboard. Verify the new actions appear in the Quick Actions grid by checking that the `_buildPanel()` method's rendering loop continues to work:
```javascript
for (const a of ACTIONS) {
    const card = actionsGrid.createEl('div', { cls: 'paperforge-action-card' });
    // ... render logic ...
}
```
Any new ACTIONS entries are automatically picked up by this loop.
</action>

<acceptance_criteria>
- Multiple rapid clicks on same action card are blocked (running guard)
- Missing zotero_key shows specific error notice
- Missing active file shows specific error notice  
- Large collection context uses higher timeout (60000ms)
- Invalid JSON from context command shows specific error message
- All existing actions (sync, ocr, doctor, repair) continue to work unchanged
- New actions appear correctly in the Quick Actions grid
</acceptance_criteria>

<verify>
  <automated>MISSING — Manual Obsidian verification needed. Test scenarios: (1) Click "Copy Context" with no active note — should show error. (2) Click "Copy Context" on a paper without zotero_key — should show error. (3) Click rapidly twice on "Copy Context" — second click should be blocked. (4) Click "Copy Collection Context" — should copy valid JSON array.</automated>
</verify>

<done>
Error handling and edge cases covered for both new actions. Existing actions unaffected. Rapid-click guard prevents double-execution.
</done>
</task>

</tasks>

<verification>
1. Manual: Open Obsidian with plugin loaded. Verify dashboard shows all 6 Quick Actions (4 existing + 2 new).
2. Manual: Open a paper note with zotero_key in frontmatter. Click "Copy Context". Paste clipboard — should see valid JSON with provenance and readiness blocks.
3. Manual: Open Command Palette (Ctrl+P), search "Copy Context" — both entries should appear.
4. Manual: Click "Copy Collection Context". Verify clipboard content is a JSON array.
5. Manual: Try clicking "Copy Context" with no active note — should show error notice.
6. Plugin loads without errors: check Developer Console (Ctrl+Shift+I) for any JS errors.
</verification>

<success_criteria>
- D-07: "Copy Context" button copies single-paper canonical index entry JSON to clipboard
- D-08: "Copy Collection Context" button copies filtered/all entries JSON array to clipboard
- AIC-02: User can generate a traceable context pack for a single paper from the plugin
- AIC-03: User can generate a collection-level context pack from the plugin
- AIC-04: Blocking factors and provenance are included in the output
- All 4 existing Quick Actions continue to function correctly
</success_criteria>

<output>
After completion, create `.planning/phases/26-traceable-ai-context-packs/26-03-SUMMARY.md`
</output>
