# Project Items And Inline Card Detail Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Show imported project items immediately after import, improve collection row readability, and render item detail inline under the active entry instead of in a fixed detail area. **Architecture:** Normalize project view data around a single `items` list in `ProjectService`, with each item carrying `card_status` plus card fields when available. Update the center panel renderer in the inline template script to render both pending and done items, and expand one item’s detail inline at a time. **Tech Stack:** Python, FastAPI template rendering, inline HTML/CSS/JavaScript, pytest --- ### Task 1: Define Pending-Item Project View Contract **Files:** - Modify: `tests/test_projects.py` - Modify: `tests/test_api.py` - Test: `tests/test_projects.py` - Test: `tests/test_api.py` - [ ] **Step 1: Write the failing test** Add to `tests/test_projects.py`: ```python def test_add_item_to_project_returns_pending_item_when_card_not_generated(tmp_path: Path) -> None: config = AppConfig(workspace_dir=tmp_path / "workspace", zotero_data_dir=tmp_path / "zotero") workspace = Workspace(config) workspace.create_project("thesis-ch2", "Thesis Chapter 2", "openai", "gpt-5-mini") items_index_path = config.workspace_dir / "library" / "index" / "items.json" items_index_path.parent.mkdir(parents=True, exist_ok=True) items_index_path.write_text( json.dumps( { "PAPER0001": { "item_key": "PAPER0001", "title": "Pending Item", "creators": ["Alice Smith"], "year": "2024", "item_type": "journalArticle", } }, ensure_ascii=False, ), encoding="utf-8", ) service = ProjectService(config.workspace_dir) payload = service.add_items("thesis-ch2", ["PAPER0001"]) assert payload["selected_items"] == ["PAPER0001"] assert len(payload["items"]) == 1 assert payload["items"][0]["item_key"] == "PAPER0001" assert payload["items"][0]["card_status"] == "pending" ``` Add to `tests/test_api.py`: ```python def test_import_item_keys_endpoint_returns_pending_item_in_project_view(tmp_path: Path) -> None: config = make_test_config(tmp_path) config.bridge_file.unlink() app = create_app(config, llm_client=FakeLlmClient()) create_project = _route(app, "/api/projects", "POST") import_keys = _route(app, "/api/projects/{project_id}/imports/item-keys", "POST") create_project( CreateProjectRequest( project_id="thesis-ch2", name="Thesis Chapter 2", llm_provider="openai", llm_model="gpt-5-mini", ) ) payload = import_keys("thesis-ch2", {"item_keys": ["PAPER0001"]}) assert payload["project_view"]["selected_items"] == ["PAPER0001"] assert payload["project_view"]["items"][0]["item_key"] == "PAPER0001" assert payload["project_view"]["items"][0]["card_status"] == "pending" ``` - [ ] **Step 2: Run test to verify it fails** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py -q` Expected: FAIL because project view currently only returns `cards`, not normalized `items`. - [ ] **Step 3: Write minimal implementation** Modify `src/zotero_kb/projects.py` so `_rebuild_project_index()` returns normalized items for both pending and done entries: ```python def _rebuild_project_index(self, project_id: str, selected_items: list[str]) -> dict[str, object]: cards_index = self._read_json(self.index_dir / "cards.json") items_index = self._read_json(self.index_dir / "items.json") collections_index = self._read_json(self.index_dir / "collections.json") project_items: list[dict[str, object]] = [] project_cards: list[dict[str, object]] = [] for item_key in selected_items: item_data = items_index.get(item_key, {}) card_data = cards_index.get(item_key) item_payload = { "item_key": item_key, "title": item_data.get("title", "Untitled"), "creators": item_data.get("creators", []), "year": item_data.get("year"), "item_type": item_data.get("item_type", "unknown"), "card_status": "done" if card_data else "pending", "summary": card_data.get("summary") if card_data else None, "claims": card_data.get("claims", []) if card_data else [], "quotable_spans": card_data.get("quotable_spans", []) if card_data else [], } project_items.append(item_payload) if card_data: project_cards.append(card_data) project_collections = [ payload for payload in collections_index.values() if set(payload.get("item_keys", [])) & set(selected_items) ] payload = { "project_id": project_id, "selected_items": selected_items, "items": project_items, "cards": project_cards, "collections": project_collections, } ``` - [ ] **Step 4: Run test to verify it passes** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add tests/test_projects.py tests/test_api.py src/zotero_kb/projects.py git commit -m "feat: add pending project items to project view" ``` ### Task 2: Define UI Contract For Inline Project Item Detail **Files:** - Modify: `tests/test_ui.py` - Test: `tests/test_ui.py` - [ ] **Step 1: Write the failing test** Add to `tests/test_ui.py`: ```python def test_index_has_project_item_list_and_inline_detail_hooks(tmp_path) -> None: html = _get_index_html(tmp_path) assert 'id="project-item-list"' in html assert "expandedProjectItemKey" in html assert "function renderProjectItems(items)" in html assert "function toggleProjectItemDetail(itemKey)" in html assert 'class="project-item-detail"' in html ``` ```python def test_index_collection_rows_use_readable_unselected_text(tmp_path) -> None: html = _get_index_html(tmp_path) assert ".project-item strong" in html assert ".project-item .meta" in html assert ".collection-row-toggle" in html ``` - [ ] **Step 2: Run test to verify it fails** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q` Expected: FAIL because the page still renders only cards and uses the fixed detail region. - [ ] **Step 3: Write minimal implementation** Update `src/zotero_kb/templates/index.html`: - replace the center list container with: ```html
``` - replace the fixed detail surface with a passive note: ```html

项目状态

导入后,项目条目会立即出现在上方列表;点击条目可就地展开详情。
``` - add state: ```javascript expandedProjectItemKey: null, ``` - add renderers: ```javascript function toggleProjectItemDetail(itemKey) { state.expandedProjectItemKey = state.expandedProjectItemKey === itemKey ? null : itemKey; renderProjectItems(state.currentProjectItems); } ``` ```javascript function renderProjectItems(items) { state.currentProjectItems = items || []; if (!state.currentProjectItems.length) { elements.projectItemList.innerHTML = '
当前项目还没有条目。先导入 Zotero 文献。
'; return; } elements.projectItemList.innerHTML = ""; for (const item of state.currentProjectItems) { const card = document.createElement("div"); card.className = "card-item"; const statusLabel = item.card_status === "done" ? "已生成卡片" : "未生成卡片"; card.innerHTML = ` ${escapeHtml(item.title)}
${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}

${item.card_status === "done" ? (item.summary || "暂无摘要") : "已导入项目,尚未生成卡片。"}

`; const [detailButton, statusButton, removeButton] = card.querySelectorAll("button"); detailButton.addEventListener("click", () => toggleProjectItemDetail(item.item_key)); statusButton.disabled = true; removeButton.addEventListener("click", () => removeCard(item.item_key)); if (state.expandedProjectItemKey === item.item_key) { const detail = document.createElement("div"); detail.className = "project-item-detail"; detail.innerHTML = item.card_status === "done" ? `
${item.item_key}

${item.summary || "暂无摘要"}

Claims

Quotable

` : `
${item.item_key} · ${item.year || "-"} · ${item.item_type || "-"}

这篇文献已经导入到当前项目,但还没有生成卡片内容。

`; card.appendChild(detail); } elements.projectItemList.appendChild(card); } } ``` - add CSS: ```css .project-item strong, .project-item .meta { color: var(--ink); } .project-item.active strong, .project-item.active .meta { color: #f5f7ef; } .collection-row-toggle { color: var(--accent-strong); font-weight: 700; } .project-item-detail { margin-top: 0.85rem; padding-top: 0.85rem; border-top: 1px solid var(--line); } ``` - [ ] **Step 4: Run test to verify it passes** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add tests/test_ui.py src/zotero_kb/templates/index.html git commit -m "feat: render project items with inline detail" ``` ### Task 3: Wire Project Item Rendering Into Import And Project Selection Flows **Files:** - Modify: `src/zotero_kb/templates/index.html` - Test: `tests/test_api.py` - Test: `tests/test_ui.py` - [ ] **Step 1: Write the failing test** Reuse the UI/API failures from Tasks 1 and 2. No new production code first. - [ ] **Step 2: Run tests to verify the current behavior fails** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py tests/test_ui.py -q` Expected: FAIL until the project list renderer uses `items`. - [ ] **Step 3: Write minimal implementation** Update the inline script in `src/zotero_kb/templates/index.html`: - state: ```javascript currentProjectItems: [], ``` - elements: ```javascript projectItemList: document.getElementById("project-item-list"), ``` - remove `showCardDetail()` and stop using `elements.cardDetail` - change empty-state and load paths: ```javascript async function selectProject(projectId, rerender = true) { state.currentProjectId = projectId; updateImportActionState(); if (rerender) { renderProjects(); } const payload = await api(`/api/projects/${projectId}/cards`); renderProjectItems(payload.items || []); } ``` ```javascript renderProjectItems(payload.project_view?.items || []); ``` ```javascript renderProjectItems([]); ``` - [ ] **Step 4: Run full targeted tests** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_projects.py tests/test_api.py tests/test_ui.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add src/zotero_kb/templates/index.html tests/test_projects.py tests/test_api.py tests/test_ui.py git commit -m "feat: show imported project items before card generation" ``` ### Task 4: Final Verification **Files:** - Modify: none - Test: `tests/test_projects.py` - Test: `tests/test_api.py` - Test: `tests/test_ui.py` - [ ] **Step 1: Run full test suite** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q` Expected: PASS - [ ] **Step 2: Run JS syntax guard** Run: `UV_CACHE_DIR=/tmp/uv-cache uv run pytest tests/test_ui.py::test_index_inline_script_is_valid_javascript -q` Expected: PASS - [ ] **Step 3: Manual verification** Run: ```bash UV_CACHE_DIR=/tmp/uv-cache uv run python main.py ``` Manual checks: - unselected collections remain readable in the import window - importing an item into a project makes it appear immediately in the center panel - pending item detail expands inline below the clicked item - generated item detail expands inline below the clicked item - [ ] **Step 4: Commit** ```bash git add src/zotero_kb/projects.py src/zotero_kb/templates/index.html tests/test_projects.py tests/test_api.py tests/test_ui.py docs/superpowers/specs/2026-04-16-project-items-and-inline-card-detail-design.md docs/superpowers/plans/2026-04-16-project-items-and-inline-card-detail.md git commit -m "feat: show project items inline before card generation" ```