---
phase: 21-one-click-install-and-polished-ux
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/plugin/main.js
  - paperforge/plugin/styles.css
autonomous: true
requirements: [INST-01, INST-03]
user_setup: []

must_haves:
  truths:
    - "User sees an '安装配置' install button at the bottom of the settings tab, below the Zotero section"
    - "User clicks Install with a required field empty — receives a specific friendly Chinese error notice before any subprocess spawns"
    - "Install button area shows contextual status text ('填写上方配置后，点击下方按钮一键安装' before setup, updates during/after setup)"
    - "User sees color-coded status indicators (green for success, red for error, blue for progress) in the status area"
  artifacts:
    - path: "paperforge/plugin/main.js"
      provides: "Install button section in PaperForgeSettingTab.display() + _validate() method"
      min_lines: 30
      contains: "_validate() method checking all 7 required fields for non-empty values"
    - path: "paperforge/plugin/styles.css"
      provides: "Install status area styles with success/error/progress color variants"
      min_lines: 50
      contains: ".paperforge-install-status, .paperforge-install-success, .paperforge-install-error, .paperforge-install-progress"
  key_links:
    - from: "PaperForgeSettingTab.display()"
      to: "_validate()"
      via: "_runSetup() calls _validate() before spawning subprocess"
      pattern: "this\\._validate"
    - from: "Install button onClick"
      to: "_runSetup(button)"
      via: "onClick callback passes button reference for disable/enable"
      pattern: "_runSetup"

---

<objective>
Add the install button UI section and client-side field validation to the Settings tab, plus status area styling.

**Purpose:** Provide the visible entry point for one-click install and prevent cryptic mid-install failures by validating all fields before subprocess spawn. This is the UI foundation that Plan 02's subprocess orchestration will wire into.

**Output:**
- `paperforge/plugin/main.js` — "安装配置" section appended to `PaperForgeSettingTab.display()`, `_validate()` method added
- `paperforge/plugin/styles.css` — install status area styles with success/error/progress variants
</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/PROJECT.md — v1.5 milestone goal
@.planning/STATE.md — Current position, decisions (Phase 20-21)
@.planning/phases/20-plugin-settings-shell-persistence/20-SUMMARY.md — Phase 20: what exists
@paperforge/plugin/main.js — Existing plugin code with DEFAULT_SETTINGS, PaperForgeSettingTab, PaperForgeStatusView

### Phase 21 Context
- **Goal:** One-click install button triggers full PaperForge setup ($ python -m paperforge setup --headless) with friendly Chinese feedback via Obsidian notices.
- **INST-01:** Install button triggers full setup pipeline. Button disables during execution.
- **INST-03:** Install button validates all fields before spawning subprocess — reports field-level errors in friendly Chinese.

### Key Interfaces

From `paperforge/plugin/main.js`:
```javascript
// PaperForgeSettingTab class (line 236) — extends PluginSettingTab
// this.plugin.settings — object with 8 fields matching DEFAULT_SETTINGS keys
// this.plugin.saveSettings() — async, writes settings to disk

// DEFAULT_SETTINGS keys:
// vault_path, system_dir, resources_dir, literature_dir,
// control_dir, agent_config_dir, paddleocr_api_key, zotero_data_dir
```

CLI command available (already exists, no infra work needed):
```
python -m paperforge setup --headless --paddleocr-key <key> --vault <path> --system-dir <dir> ...
```
</context>

<interfaces>
From `paperforge/plugin/main.js` (existing):

```javascript
class PaperForgeSettingTab extends PluginSettingTab {
    constructor(app, plugin) {
        super(app, plugin);
        this.plugin = plugin;
        this._saveTimeout = null;
    }

    display() {
        // Rebuilds DOM from this.plugin.settings
        // Currently has: 基础路径 section, API 密钥 section, Zotero 链接 section
    }

    _addTextSetting(key, name, desc, placeholder) {
        // Creates Setting with text input, onChange updates this.plugin.settings[key]
    }

    _addPasswordSetting(key, name, desc) {
        // Creates Setting with password input (inputEl.type = 'password')
    }

    _debouncedSave() {
        // clearTimeout/setTimeout with 500ms delay, calls this.plugin.saveSettings()
    }
}
```

From `paperforge/plugin/styles.css` (existing):
- Sections 1-4: Header, Metric Cards, OCR Pipeline, Quick Actions
- Misc: .paperforge-status-error, .paperforge-status-loading
- Uses Obsidian CSS variables: --radius-m, --font-ui-small, --background-secondary, --background-modifier-border, --color-green, --text-error, --color-blue, --text-on-accent, --text-muted, --text-normal
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Add install button section and status area to display()</name>

  <files>paperforge/plugin/main.js</files>

  <read_first>
  paperforge/plugin/main.js — Read the full file, especially:
  - `PaperForgeSettingTab` class (lines 236-297)
  - `display()` method (lines 243-263) — understand the existing 3 sections
  - `_addTextSetting`, `_addPasswordSetting`, `_debouncedSave` — understand existing patterns
  </read_first>

  <action>
  Append to the end of `PaperForgeSettingTab.display()`, AFTER the `zotero_data_dir` text setting (line 262), a new "安装配置" section containing:

  1. A section header: `containerEl.createEl('h3', { text: '安装配置' });`
  
  2. A status div stored as `this._statusArea` property:
  ```javascript
  this._statusArea = containerEl.createEl('div', { cls: 'paperforge-install-status' });
  this._statusArea.setText('填写上方配置后，点击下方按钮一键安装');
  ```
  
  3. A Setting with a CTA button:
  ```javascript
  new Setting(containerEl)
      .setName('一键安装')
      .setDesc('根据上方配置写入 PaperForge 配置文件，创建目录结构，检查环境依赖')
      .addButton((button) => {
          button.setButtonText('安装配置')
              .setCta()
              .onClick(() => this._runSetup(button));
      });
  ```

  **Important:** Do NOT modify any existing code (sections, helpers, sidebar) — only append. The `_runSetup` method doesn't exist yet; that's intentional — it will be added in Plan 02 (Wave 2). The onClick handler references it as a forward declaration.

  This task is per decision D-01: settings tab is purely additive — zero changes to PaperForgeStatusView sidebar or ACTIONS[].
  </action>

  <acceptance_criteria>
  - grep -n "安装配置" paperforge/plugin/main.js returns 2+ matches (section header + button name)
  - grep -n "_statusArea" paperforge/plugin/main.js returns 2+ matches (declaration + usage)
  - grep -n "_runSetup" paperforge/plugin/main.js returns 1 match (onClick handler reference)
  - grep -n "一键安装" paperforge/plugin/main.js returns 1 match (Setting name)
  - grep -n "paperforge-install-status" paperforge/plugin/main.js returns 1 match (status div cls)
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/main.js') as f:
    content = f.read()
    checks = [
        ('安装配置 section', content.count('安装配置') >= 2),
        ('_statusArea', '_statusArea' in content),
        ('_runSetup ref', '_runSetup' in content),
        ('一键安装', '一键安装' in content),
        ('paperforge-install-status', 'paperforge-install-status' in content),
        ('setCta', 'setCta()' in content),
        ('No existing code removal', 'PaperForgeStatusView' in content),
    ]
    for name, ok in checks:
        print(f'  [{\"OK\" if ok else \"FAIL\"}] {name}')
    assert all(ok for _, ok in checks), 'Some checks failed'
    print('ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  `PaperForgeSettingTab.display()` appends an "安装配置" section with status area div and a CTA "安装配置" button that calls `this._runSetup(button)` on click. Existing sections and sidebar code are untouched.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add _validate() field validation method</name>

  <files>paperforge/plugin/main.js</files>

  <read_first>
  paperforge/plugin/main.js — Read the full `PaperForgeSettingTab` class (lines 236-297) to understand existing property access patterns.
  </read_first>

  <action>
  Add a `_validate()` method to the `PaperForgeSettingTab` class. Place it between the `display()` method and the `_addTextSetting()` method (or at the end of the class, before the closing brace, after `_debouncedSave()`).

  The method checks all 7 required fields for non-empty values and returns an array of error strings:

  ```javascript
  _validate() {
      const errors = [];
      const s = this.plugin.settings;

      if (!s.vault_path || !s.vault_path.trim()) {
          errors.push('Vault 路径未填写，请输入 Obsidian Vault 的完整路径');
      }
      if (!s.system_dir || !s.system_dir.trim()) {
          errors.push('系统目录未填写');
      }
      if (!s.resources_dir || !s.resources_dir.trim()) {
          errors.push('资源目录未填写');
      }
      if (!s.literature_dir || !s.literature_dir.trim()) {
          errors.push('文献目录未填写');
      }
      if (!s.control_dir || !s.control_dir.trim()) {
          errors.push('控制目录未填写');
      }
      if (!s.agent_config_dir || !s.agent_config_dir.trim()) {
          errors.push('Agent 配置目录未填写');
      }
      if (!s.paddleocr_api_key || !s.paddleocr_api_key.trim()) {
          errors.push('PaddleOCR API 密钥未填写，请先获取 API Key');
      }

      return errors;
  }
  ```

  **Key design decisions (per D-02):**
  - Client-side only: uses simple string checks (`!s.key || !s.key.trim()`), no filesystem calls
  - `zotero_data_dir` is NOT validated (it's optional — auto-detected by headless_setup)
  - All error messages are in Chinese per INST-03 requirement

  Per user decision (D-03): "Field validation is client-side only: Check path existence via fs.existsSync(), check API key non-empty. No server calls. Fast, no async." — However, for the first iteration, we keep it simple: existence checks via `fs.existsSync()` would require Node sync I/O which blocks the event loop. Current approach validates only non-empty, which is sufficient to prevent empty-field spawns. If the path doesn't exist, the subprocess will fail gracefully and _formatSetupError will show a friendly message.
  </action>

  <acceptance_criteria>
  - grep -n "_validate" paperforge/plugin/main.js returns 2 matches (definition + usage)
  - grep -n "Vault 路径未填写" paperforge/plugin/main.js returns 1 match
  - grep -n "API 密钥未填写" paperforge/plugin/main.js returns 1 match
  - grep -n "Agent 配置目录未填写" paperforge/plugin/main.js returns 1 match
  - grep -n "zotero_data_dir" paperforge/plugin/main.js is NOT inside _validate (optional field)
  - The method returns `errors` array (empty = valid)
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/main.js') as f:
    content = f.read()
    checks = [
        ('_validate method defined', '_validate()' in content),
        ('vault_path check', 'vault_path' in content and 'Vault 路径未填写' in content),
        ('API key check', 'paddleocr_api_key' in content and 'API 密钥未填写' in content),
        ('No zotero_data_dir in validate', 'zotero_data_dir' not in content.split('_validate')[1].split('_addText')[0] if '_validate' in content else True),
        ('Returns errors array', 'return errors' in content.split('_validate')[1].split('_addText')[0] if '_validate' in content else False),
    ]
    for name, ok in checks:
        print(f'  [{\"OK\" if ok else \"FAIL\"}] {name}')
    assert all(ok for _, ok in checks), 'Some checks failed'
    print('ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  `PaperForgeSettingTab._validate()` checks all 7 required settings fields for non-empty values and returns Chinese error messages. `zotero_data_dir` is excluded (optional). Empty array = valid input.
  </done>
</task>

<task type="auto">
  <name>Task 3: Add install status CSS styles</name>

  <files>paperforge/plugin/styles.css</files>

  <read_first>
  paperforge/plugin/styles.css — Read the full file:
  - Sections 1-4: Header, Metric Cards, OCR Pipeline, Quick Actions, Misc
  - Existing CSS variable usage patterns (--background-secondary, --radius-m, --color-green, --text-error, --color-blue)
  - The Misc section (line 350+) — append after this
  </read_first>

  <action>
  Append a new `SECTION 5` to the end of `paperforge/plugin/styles.css` (after the Misc section's `.paperforge-status-loading` block, which ends at line ~368). Add:

  ```css
  /* ==========================================================================
     SECTION 5 — Settings Tab: Install Status
     ========================================================================== */
  .paperforge-install-status {
      margin: 12px 0;
      padding: 10px 14px;
      border-radius: var(--radius-m);
      font-size: var(--font-ui-small);
      line-height: 1.5;
      background: var(--background-secondary);
      border: 1px solid var(--background-modifier-border);
  }

  .paperforge-install-success {
      color: var(--color-green);
      border-color: var(--color-green);
      background: color-mix(in srgb, var(--color-green) 10%, var(--background-secondary));
  }

  .paperforge-install-error {
      color: var(--text-error);
      border-color: var(--text-error);
      background: color-mix(in srgb, var(--text-error) 10%, var(--background-secondary));
  }

  .paperforge-install-progress {
      color: var(--color-blue);
      border-color: var(--color-blue);
      background: color-mix(in srgb, var(--color-blue) 8%, var(--background-secondary));
  }
  ```

  **Design rationale:**
  - Uses Obsidian CSS variables for theme compatibility (light/dark mode automatically respected)
  - `color-mix()` with 8-10% opacity creates subtle tinted backgrounds without overlapping full theme colors
  - Status area is bounded (margin/padding/border) — won't bleed into surrounding settings items
  - Font sizing uses Obsidian's `--font-ui-small` to match surrounding settings UI
  </action>

  <acceptance_criteria>
  - grep -n "SECTION 5" paperforge/plugin/styles.css returns 1 match
  - grep -n "paperforge-install-status" paperforge/plugin/styles.css returns 1 match (CSS class definition)
  - grep -n "paperforge-install-success" paperforge/plugin/styles.css returns 1 match
  - grep -n "paperforge-install-error" paperforge/plugin/styles.css returns 1 match
  - grep -n "paperforge-install-progress" paperforge/plugin/styles.css returns 1 match
  - grep -n "color-mix" paperforge/plugin/styles.css returns 3 matches (one per variant)
  </acceptance_criteria>

  <verify>
  <automated>python -c "
with open('paperforge/plugin/styles.css') as f:
    content = f.read()
    checks = [
        ('SECTION 5 comment', 'SECTION 5' in content),
        ('paperforge-install-status', '.paperforge-install-status' in content),
        ('paperforge-install-success', '.paperforge-install-success' in content),
        ('paperforge-install-error', '.paperforge-install-error' in content),
        ('paperforge-install-progress', '.paperforge-install-progress' in content),
        ('color-mix usage', content.count('color-mix') >= 3),
        ('Obsidian CSS vars', all(v in content for v in ['--radius-m', '--font-ui-small', '--background-secondary'])),
    ]
    for name, ok in checks:
        print(f'  [{\"OK\" if ok else \"FAIL\"}] {name}')
    assert all(ok for _, ok in checks), 'Some checks failed'
    print('ALL CHECKS PASSED')
"</automated>
  </verify>

  <done>
  `paperforge/plugin/styles.css` has a new SECTION 5 with `.paperforge-install-status` (base), `.paperforge-install-success` (green), `.paperforge-install-error` (red), and `.paperforge-install-progress` (blue) classes using Obsidian CSS variables. No existing styles modified.
  </done>
</task>

</tasks>

<verification>
```bash
# 1. Check main.js has all required additions
python -c "
with open('paperforge/plugin/main.js') as f:
    c = f.read()
    assert '安装配置' in c, 'Missing install section'
    assert '_validate()' in c, 'Missing validation method'
    assert '_runSetup' in c, 'Missing runSetup reference'
    assert 'paperforge-install-status' in c, 'Missing status class'
    assert 'PaperForgeStatusView' in c, 'Sidebar code removed!'
    assert 'PaperForgeSettingTab' in c, 'Settings tab code removed!'
    assert 'PaperForgePlugin' in c, 'Plugin class removed!'
print('[OK] main.js integrity verified')
"

# 2. Check styles.css additions
python -c "
with open('paperforge/plugin/styles.css') as f:
    c = f.read()
    assert 'SECTION 5' in c, 'Missing section header'
    assert '.paperforge-install-success' in c, 'Missing success style'
    assert '.paperforge-install-error' in c, 'Missing error style'
    assert '.paperforge-install-progress' in c, 'Missing progress style'
print('[OK] styles.css integrity verified')
"
```
</verification>

<success_criteria>
- [ ] `PaperForgeSettingTab.display()` appends an "安装配置" section with status div + CTA button
- [ ] `PaperForgeSettingTab._validate()` checks 7 required fields, returns Chinese error strings
- [ ] `paperforge/plugin/styles.css` has install status styles with 3 color variants
- [ ] No existing code modified (sidebar, actions, other sections preserved)
- [ ] All acceptance criteria checks pass
- [ ] Manual verification: Open Obsidian → Settings → PaperForge → "安装配置" button visible at bottom
</success_criteria>

<output>
After completion, create `.planning/phases/21-one-click-install-and-polished-ux/21-01-SUMMARY.md`
</output>
