diff --git a/.claude/agents/gsd-debugger.md b/.claude/agents/gsd-debugger.md index e22b61f64a..7aaac42bb8 100644 --- a/.claude/agents/gsd-debugger.md +++ b/.claude/agents/gsd-debugger.md @@ -843,6 +843,21 @@ ls .planning/debug/*.md 2>/dev/null | grep -v resolved - Continue to create_debug_file + +Before investigating, scan `.learnings/` for entries relevant to the reported issue: + +```bash +ls .learnings/*.md 2>/dev/null | grep -v README +``` + +If entries exist, quickly scan titles and read any that relate to the affected area (e.g., same component, similar error type, related feature). Extract: +- Known gotchas in the affected area +- Past bugs with similar symptoms +- Key files that were involved in previous issues + +This takes ~30 seconds and can shortcut investigation. Skip if no `.learnings/` directory exists. + + **Create debug file IMMEDIATELY.** diff --git a/.claude/agents/gsd-executor.md b/.claude/agents/gsd-executor.md index 720a43db40..bc2a046d66 100644 --- a/.claude/agents/gsd-executor.md +++ b/.claude/agents/gsd-executor.md @@ -41,6 +41,21 @@ Options: **If .planning/ doesn't exist:** Error - project not initialized. + +Scan `.learnings/` for entries relevant to the current plan's domain: + +```bash +ls .learnings/*.md 2>/dev/null | grep -v README +``` + +If entries exist, quickly scan titles and read any that relate to the current phase/task area (e.g., same components, similar libraries, related features). Extract: +- Gotchas and edge cases to watch for +- Patterns that worked or failed +- Key files to be aware of + +This takes ~30 seconds and prevents repeating past mistakes. Skip if no `.learnings/` directory exists. + + Read the plan file provided in your prompt context. diff --git a/.claude/agents/gsd-phase-researcher.md b/.claude/agents/gsd-phase-researcher.md index b0e9acd15e..50fd42b7d5 100644 --- a/.claude/agents/gsd-phase-researcher.md +++ b/.claude/agents/gsd-phase-researcher.md @@ -497,6 +497,19 @@ cat "${PHASE_DIR}"/*-CONTEXT.md 2>/dev/null Parse CONTEXT.md content before proceeding to research. +**Scan learnings (RECOMMENDED):** + +```bash +ls .learnings/*.md 2>/dev/null | grep -v README +``` + +If entries exist, quickly scan titles and read any that relate to the current phase's technology domain. Extract: +- Past discoveries about libraries or APIs being researched +- Gotchas that should inform research direction +- Things that looked right but caused issues + +This prevents re-discovering known issues and focuses research on genuinely unknown areas. Skip if no `.learnings/` directory exists. + ## Step 2: Identify Research Domains Based on phase description, identify what needs investigating: diff --git a/.claude/agents/gsd-planner.md b/.claude/agents/gsd-planner.md index 6264d7ad8f..70c6853334 100644 --- a/.claude/agents/gsd-planner.md +++ b/.claude/agents/gsd-planner.md @@ -1000,6 +1000,22 @@ Read `.planning/STATE.md` and parse: If STATE.md missing but .planning/ exists, offer to reconstruct or continue without. + +Scan `.learnings/` for entries relevant to the phase being planned: + +```bash +ls .learnings/*.md 2>/dev/null | grep -v README +``` + +If entries exist, quickly scan titles and read any that relate to the current phase's domain (e.g., same technology area, similar components, related features). Extract: +- Gotchas to build into plan verification steps +- Patterns that worked — reuse in task actions +- Patterns that failed — explicitly avoid in task actions +- Key files to reference in plan context + +Integrate relevant learnings into the plan as constraints or guidance. Skip if no `.learnings/` directory exists. + + Check for codebase map: diff --git a/.claude/commands/gsd/quick.md b/.claude/commands/gsd/quick.md index 3efbb78693..87e0975466 100644 --- a/.claude/commands/gsd/quick.md +++ b/.claude/commands/gsd/quick.md @@ -83,6 +83,14 @@ Generate slug from description: slug=$(echo "$DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) ``` +**Scan learnings** for relevant context: + +```bash +ls .learnings/*.md 2>/dev/null | grep -v README +``` + +If entries exist, quickly scan titles and read any that relate to the task description. Extract gotchas, patterns, and key files that may be relevant. This prevents repeating past mistakes. Skip if no `.learnings/` directory exists. + --- ### Step 2b: Detect UI task and offer design workflow @@ -516,6 +524,38 @@ Get final commit hash: commit_hash=$(git rev-parse --short HEAD) ``` +**Capture learnings** (skip if trivial): + +If the quick task involved non-obvious discoveries, gotchas, or useful patterns, create a learnings entry at `.learnings/YYYY-MM-DD-brief-description.md`: + +```markdown +# [Brief Title] + +**Date:** YYYY-MM-DD + +## Original Prompt + +> [User's original task description] + +## What I Learned + +- [Non-obvious discoveries, gotchas, edge cases] + +## What Would Have Helped + +- [Missing context, files to check first] + +## Key Files + +- [Relevant files for future reference] +``` + +If created, commit it: +```bash +git add .learnings/ +git commit -m "docs(learnings): quick-${next_num} - {brief description}" +``` + Display completion output: ```text diff --git a/.claude/get-shit-done/workflows/complete-milestone.md b/.claude/get-shit-done/workflows/complete-milestone.md index 384a5299fa..8879ba4c58 100644 --- a/.claude/get-shit-done/workflows/complete-milestone.md +++ b/.claude/get-shit-done/workflows/complete-milestone.md @@ -661,6 +661,44 @@ Confirm: "Committed: chore: complete v[X.Y] milestone" + +**Capture milestone-level learnings.** + +Create a learnings entry at `.learnings/YYYY-MM-DD-milestone-vXY-brief-description.md` reflecting on the entire milestone: + +```markdown +# Milestone v[X.Y] - Learnings + +**Date:** YYYY-MM-DD + +## Original Prompt + +> [Milestone goal/description from PROJECT.md] + +## What I Learned + +- [Architectural patterns that worked well or poorly across phases] +- [Cross-phase gotchas and integration issues] +- [Libraries, tools, or approaches worth reusing or avoiding] + +## What Would Have Helped + +- [Context that was missing at milestone start] +- [Planning gaps that caused rework] +- [Questions that should have been asked during requirements] + +## Key Files + +- [Files most critical to this milestone for future reference] +``` + +Commit the learnings file: +```bash +git add .learnings/ +git commit -m "docs(learnings): milestone v[X.Y] - {brief description}" +``` + + ``` diff --git a/.claude/get-shit-done/workflows/execute-phase.md b/.claude/get-shit-done/workflows/execute-phase.md index 5c214dc99e..8c327c3172 100644 --- a/.claude/get-shit-done/workflows/execute-phase.md +++ b/.claude/get-shit-done/workflows/execute-phase.md @@ -464,6 +464,48 @@ git commit -m "docs(phase-{X}): complete phase execution" ``` + +**Capture learnings from this phase execution.** + +Reflect on what was learned and create a learnings entry at `.learnings/YYYY-MM-DD-phase-{X}-brief-description.md`. + +Use this template: + +```markdown +# [Phase Name] - Learnings + +**Date:** YYYY-MM-DD + +## Original Prompt + +> [Phase goal from ROADMAP.md] + +## What I Learned + +- [Non-obvious discoveries about the codebase, libraries, or patterns] +- [Gotchas and edge cases encountered] +- [Things that look similar but behave differently] + +## What Would Have Helped + +- [Missing context that slowed things down] +- [Files that should have been checked first] +- [Clarifying questions that should have been asked earlier] + +## Key Files + +- [Files most relevant to this type of task for future reference] +``` + +Commit the learnings file: +```bash +git add .learnings/ +git commit -m "docs(learnings): phase {X} - {brief description}" +``` + +**Skip this step if the phase was trivial or no meaningful learnings emerged.** + + Present next steps based on milestone status: diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 0ee178b5ae..318bef1ef3 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -70,6 +70,11 @@ reviews: - '!**/pnpm-lock.yaml' - '!**/*.lock' - '!**/generated/**' + - '!.claude/agents/**' + - '!.claude/commands/**' + - '!.claude/get-shit-done/**' + - '!.learnings/**' + - '!.planning/**' chat: # Enable chat interactions in PR comments diff --git a/.learnings/2026-02-07-empty-state-upload-regression.md b/.learnings/2026-02-07-empty-state-upload-regression.md new file mode 100644 index 0000000000..ae2907f730 --- /dev/null +++ b/.learnings/2026-02-07-empty-state-upload-regression.md @@ -0,0 +1,32 @@ +# Empty State Upload Zone Regression + +**Date:** 2026-02-07 + +## Original Prompt + +> "there is a dropzone underneath the ascii art, and I am not a fan of the ascii art chosen. surely you can do something better" + +Followed by: "the drag and drop to upload is no longer working" + +## What I Learned + +- **Removing a "visual" component can break functionality:** The UploadZone in EmptyState looked like a purely visual element (a box with upload text), but it was also the react-dropzone drop target providing drag-and-drop upload. Removing it to clean up the UI killed the DnD functionality. +- **Components often serve dual purposes:** Before removing any component, check if it provides event handlers, drop targets, focus management, or other invisible behavior beyond its visual output. +- **The fix pattern for invisible drop targets:** Use `useDropzone` directly on the container div via `getRootProps()` — this makes the entire area a drop target without rendering a separate visible upload zone component. Apply `getInputProps()` for the hidden file input. +- **API script naming:** The API uses `pnpm --filter api dev` (not `start:dev`). The `dev` script runs `nest start --watch`. +- **Docker is unnecessary in this dev environment:** PostgreSQL and IPFS are already exposed on the host. Just start the API and frontend. +- **GSD vendor markdown files fail markdownlint:** The `.claude/agents/gsd-*.md` and workflow files use non-standard markdown (HTML-like `` tags, unfenced code blocks). These need to be excluded from linting via `.markdownlintignore`. + +## What Would Have Helped + +- Reading the UploadZone component before removing it from EmptyState to understand it provided drop target functionality, not just visuals +- Verifying DnD still worked immediately after the cosmetic fix, before committing +- A regression test for drag-and-drop upload in empty folders + +## Key Files + +- `apps/web/src/components/file-browser/EmptyState.tsx` — empty folder display + drop target +- `apps/web/src/components/file-browser/UploadZone.tsx` — standalone upload zone with react-dropzone +- `apps/web/src/components/file-browser/FileBrowser.tsx` — renders EmptyState with folderId prop +- `apps/web/src/styles/file-browser.css` — `.empty-state` and `.empty-state-drag-active` styles +- `tests/e2e/.env` — Web3Auth test credentials for Playwright login diff --git a/.learnings/README.md b/.learnings/README.md new file mode 100644 index 0000000000..b85d46fb65 --- /dev/null +++ b/.learnings/README.md @@ -0,0 +1,54 @@ +# Agent Learnings + +This folder contains learnings from AI-assisted development sessions. Each entry captures insights that help future sessions work more effectively in this codebase. + +## When to Create an Entry + +Create an entry after completing a significant task that involved: +- Learning something non-obvious about the codebase +- Getting stuck and finding a solution +- Discovering patterns or gotchas worth remembering + +## File Naming + +`YYYY-MM-DD-brief-description.md` + +Example: `2026-02-06-discord-style-usernames.md` + +## Entry Structure + +```markdown +# [Brief Title] + +**Date:** YYYY-MM-DD + +## Original Prompt + +> [Full prompt text as provided by user - preserving exact wording helps improve future prompts] + +## What I Learned + +Bullet points of non-obvious discoveries: +- Gotchas and edge cases +- Codebase patterns +- Data structure quirks +- Things that look similar but behave differently + +## What Would Have Helped + +Information that would have made the task smoother if known upfront: +- Missing context from the prompt +- Files I should have checked first +- Clarifying questions I should have asked + +## Key Files + +List of files most relevant to this type of task (for future reference). +``` + +## Guidelines + +- **Be concise** - Future you wants quick insights, not a novel +- **Focus on learnings** - Skip implementation details that are obvious from the code +- **Highlight gotchas** - The non-obvious stuff is most valuable +- **Think reusability** - What would help with similar tasks in the future? diff --git a/.markdownlintignore b/.markdownlintignore index c430e188c9..124fed8f99 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -9,3 +9,8 @@ CHANGELOG.md # Finalized specifications - do not modify 00-Preliminary-R&D/ 00_START_HERE.md + +# GSD workflow files - vendor/generated markdown with non-standard syntax +.claude/agents/gsd-*.md +.claude/commands/gsd/ +.claude/get-shit-done/ diff --git a/.planning/STATE.md b/.planning/STATE.md index 17890512a4..64e992ad26 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -184,11 +184,12 @@ Recent decisions affecting current work: ### Pending Todos -4 pending todo(s): +5 pending todo(s): - `2026-01-22-atomic-file-upload-flow.md` — Atomic file upload flow with client-side CID (area: api) - `2026-01-23-pre-upload-file-validation.md` — Pre-upload file name validation and duplicate prevention (area: ui) - `2026-01-23-simple-text-file-editor-modal.md` — Add simple text file editor modal (area: ui) +- `2026-02-07-web-worker-large-file-encryption.md` — Offload large file encryption to Web Worker (area: ui) - ~~`2026-01-21-local-ipfs-node-for-testing.md` — Add local IPFS node to Docker stack for testing (area: api)~~ - **COMPLETED: Phase 4.2** ### Blockers/Concerns @@ -205,6 +206,7 @@ Recent decisions affecting current work: | # | Description | Date | Commit | Directory | | --- | -------------------------------------- | ---------- | ------- | ----------------------------------------------------------------------------------- | | 001 | Add API status indicator on login page | 2026-01-22 | 929143e | [001-login-page-api-status-indicator](./quick/001-login-page-api-status-indicator/) | +| 002 | Fix empty state ASCII art | 2026-02-07 | ff97f12 | [002-fix-empty-state-ascii-art](./quick/002-fix-empty-state-ascii-art/) | ### Research Flags (from research phase) @@ -222,12 +224,12 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-02-02 -Stopped at: Phase 7 complete - all 4 plans executed, verified ✓ +Last session: 2026-02-07 +Stopped at: Quick task 002 complete + DnD regression fix; 6.3 UAT gap closed (12/12 pass) Resume file: None Next plan: Phase 8 (TEE Integration) --- _State initialized: 2026-01-20_ -_Last updated: 2026-02-02 after 07-04 completion (gap closure - multi-device sync working)_ +_Last updated: 2026-02-07 after 06.3 UAT gap closure (12/12 pass) + DnD regression fix_ diff --git a/.planning/phases/06.3-ui-structure-refactor/06.3-UAT.md b/.planning/phases/06.3-ui-structure-refactor/06.3-UAT.md index a22d9242b2..acaed57841 100644 --- a/.planning/phases/06.3-ui-structure-refactor/06.3-UAT.md +++ b/.planning/phases/06.3-ui-structure-refactor/06.3-UAT.md @@ -60,9 +60,10 @@ result: pass ### 10. Empty State ASCII Art expected: When viewing an empty folder, a terminal-style ASCII art folder icon is displayed with a message indicating the folder is empty. -result: issue +result: pass (fixed) reported: "there is a dropzone underneath the ascii art, and I am not a fan of the ascii art chosen. surely you can do something better" severity: cosmetic +resolution: "Replaced ASCII art with terminal-window design (ls -la / total 0), removed visible UploadZone, integrated useDropzone directly into EmptyState div for invisible drag-and-drop support. Fixed in quick task 002 + follow-up DnD regression fix." ### 11. File Operations Still Work @@ -77,19 +78,18 @@ result: pass ## Summary total: 12 -passed: 11 -issues: 1 +passed: 12 +issues: 0 pending: 0 skipped: 0 ## Gaps +_All gaps closed._ + +### Closed + - truth: "Empty folder shows a polished terminal-style ASCII art folder icon with no extraneous elements" - status: failed - reason: "User reported: there is a dropzone underneath the ascii art, and I am not a fan of the ascii art chosen. surely you can do something better" - severity: cosmetic + status: closed + resolution: "Quick task 002 replaced ASCII art with terminal-window design and removed visible UploadZone. Follow-up fix integrated useDropzone directly into EmptyState container for invisible drag-and-drop support." test: 10 - root_cause: "" - artifacts: [] - missing: [] - debug_session: "" diff --git a/.planning/quick/002-fix-empty-state-ascii-art/002-PLAN.md b/.planning/quick/002-fix-empty-state-ascii-art/002-PLAN.md new file mode 100644 index 0000000000..443ec08adb --- /dev/null +++ b/.planning/quick/002-fix-empty-state-ascii-art/002-PLAN.md @@ -0,0 +1,189 @@ +--- +phase: quick-002 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - apps/web/src/components/file-browser/EmptyState.tsx + - apps/web/src/components/file-browser/FileBrowser.tsx + - apps/web/src/styles/file-browser.css + - apps/web/src/index.css +autonomous: true + +must_haves: + truths: + - 'Empty folder shows terminal-window ASCII art with box-drawing characters' + - 'Terminal shows $ ls -la command, total 0 output, and $ cursor with block' + - 'No embedded UploadZone inside the empty state (toolbar upload is sufficient)' + - 'ASCII art is green (#00D084), label is muted (#8b9a8f), hint is dim (#4a5a4e)' + artifacts: + - path: 'apps/web/src/components/file-browser/EmptyState.tsx' + provides: 'Terminal-style empty state without UploadZone' + contains: 'ls -la' + - path: 'apps/web/src/styles/file-browser.css' + provides: 'Empty state styles without .empty-state-upload class' + key_links: + - from: 'apps/web/src/components/file-browser/FileBrowser.tsx' + to: 'EmptyState' + via: 'import and render without folderId prop' + pattern: ' +Replace the basic ASCII art box in EmptyState with a polished terminal-window design using +box-drawing characters, and remove the redundant embedded UploadZone component (the toolbar +already provides upload functionality). + +Purpose: Close the cosmetic UAT gap from Phase 6.3 -- the empty state should match the +approved "EmptyState - Improved" design from the Pencil file. + +Output: Updated EmptyState.tsx (no props, no UploadZone, terminal ASCII art), updated +FileBrowser.tsx (no folderId prop passed), updated CSS (new color tokens, removed unused class). + + + +@./.claude/get-shit-done/workflows/execute-plan.md +@./.claude/get-shit-done/templates/summary.md + + + +@apps/web/src/components/file-browser/EmptyState.tsx +@apps/web/src/components/file-browser/FileBrowser.tsx +@apps/web/src/styles/file-browser.css +@apps/web/src/index.css +@designs/cipher-box-design.pen (frame "EmptyState - Improved", node XyFqv) + + + + + + Task 1: Add CSS color tokens and update empty state styles + + apps/web/src/index.css + apps/web/src/styles/file-browser.css + + +In `apps/web/src/index.css`, add two new design tokens in the `:root` block, after the +existing `--color-text-secondary` line (around line 26): + +```css +--color-text-muted: #8b9a8f; /* Muted gray-green for labels */ +--color-text-dim: #4a5a4e; /* Dim gray-green for hints */ +``` + +In `apps/web/src/styles/file-browser.css`, make these changes to the Empty State section +(lines ~377-436): + +1. Update `.empty-state-ascii` color from `var(--color-text-secondary)` to + `var(--color-green-primary)` -- the terminal art should be bright green (#00D084). + +2. Update `.empty-state-text` color from `var(--color-text-primary)` to + `var(--color-text-muted)` -- the "// EMPTY DIRECTORY" label uses the muted tone (#8b9a8f). + +3. Update `.empty-state-hint` color from `var(--color-text-secondary)` to + `var(--color-text-dim)` -- the hint text uses the dimmer tone (#4a5a4e). + +4. DELETE the `.empty-state-upload` rule block entirely (lines ~432-434): + + ```css + .empty-state-upload { + margin-top: var(--spacing-md); + } + ``` + + This class is no longer used since UploadZone is removed from EmptyState. + + + Grep for `.empty-state-upload` in file-browser.css -- should return zero matches. + Grep for `--color-text-muted` in index.css -- should return one match. + Grep for `--color-text-dim` in index.css -- should return one match. + Grep for `color-green-primary` in the `.empty-state-ascii` rule -- should match. + + + New color tokens exist in index.css. Empty state CSS uses correct design colors. + The unused .empty-state-upload class is removed. + + + + + Task 2: Replace ASCII art and remove UploadZone from EmptyState + + apps/web/src/components/file-browser/EmptyState.tsx + apps/web/src/components/file-browser/FileBrowser.tsx + + +Rewrite `apps/web/src/components/file-browser/EmptyState.tsx`: + +1. REMOVE the `import { UploadZone }` import. +2. REMOVE the `EmptyStateProps` type and the `folderId` prop entirely. The component takes no props. +3. Replace the `asciiArt` constant with a terminal-window design using box-drawing characters. + The exact string (use template literal, no leading whitespace on the box lines): + + ```ts + const terminalArt = `┌──────────────────────┐ + │ $ ls -la │ + │ total 0 │ + │ $ █ │ + └──────────────────────┘`; + ``` + + IMPORTANT: The box must be visually aligned -- all lines the same width between the + vertical bars. Use spaces to pad each line to equal length inside the box. The block + cursor character is U+2588 (FULL BLOCK). + +4. Update the JSX: + - Replace `{asciiArt}` with `{terminalArt}` in the `
`.
+   - Change the hint text from `"drag files here or use upload"` to
+     `"drag files here or use --upload"` (with `--` prefix for terminal aesthetic).
+   - REMOVE the `
` wrapper and the `` inside it. + - The component signature becomes `export function EmptyState()` with no arguments. + +5. Update the JSDoc to reflect the component no longer accepts props or contains an upload zone. + +In `apps/web/src/components/file-browser/FileBrowser.tsx`: + +1. Find the line (around line 358): `` +2. Change it to: `` + (Remove the folderId prop since EmptyState no longer accepts it.) + + + Run `npx tsc --noEmit` from `apps/web/` to confirm no TypeScript errors. + Grep EmptyState.tsx for "UploadZone" -- should return zero matches. + Grep EmptyState.tsx for "folderId" -- should return zero matches. + Grep EmptyState.tsx for "ls -la" -- should return one match. + Grep FileBrowser.tsx for "folderId={currentFolderId}" near EmptyState -- should return zero matches. + + + EmptyState renders terminal-window ASCII art with box-drawing characters showing + `$ ls -la` / `total 0` / `$ cursor`. No UploadZone, no props. FileBrowser passes + no props to EmptyState. TypeScript compiles cleanly. + + + + + + +1. `npx tsc --noEmit` in apps/web passes (no type errors from prop removal) +2. No references to UploadZone remain in EmptyState.tsx +3. No references to folderId remain in EmptyState.tsx +4. The terminal art contains box-drawing characters and "ls -la" +5. CSS color tokens --color-text-muted and --color-text-dim exist in index.css +6. .empty-state-upload CSS class is removed from file-browser.css +7. Visual check (Playwright if available): empty state shows terminal box, green art, + muted label, dim hint text + + + + +- EmptyState component renders a terminal-window with box-drawing chars (no old box ASCII) +- Terminal shows: $ ls -la, total 0, $ cursor-block +- No UploadZone inside EmptyState (toolbar handles uploads) +- EmptyState takes no props; FileBrowser renders `` with no args +- Colors match design: ASCII art #00D084, label #8b9a8f, hint #4a5a4e +- TypeScript compilation succeeds + + + +After completion, create `.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md` + diff --git a/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md b/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md new file mode 100644 index 0000000000..197fc49e49 --- /dev/null +++ b/.planning/quick/002-fix-empty-state-ascii-art/002-SUMMARY.md @@ -0,0 +1,100 @@ +--- +phase: quick-002 +plan: 01 +subsystem: ui +tags: [css, react, ascii-art, terminal-aesthetic, empty-state] + +# Dependency graph +requires: + - phase: 06.3-ui-structure-refactor + provides: EmptyState component and file-browser CSS +provides: + - Terminal-window ASCII art empty state with box-drawing characters + - Design-accurate color tokens (--color-text-muted, --color-text-dim) +affects: [] + +# Tech tracking +tech-stack: + added: [] + patterns: + - 'Box-drawing characters for terminal UI elements' + +key-files: + created: [] + modified: + - apps/web/src/components/file-browser/EmptyState.tsx + - apps/web/src/components/file-browser/FileBrowser.tsx + - apps/web/src/styles/file-browser.css + - apps/web/src/index.css + +key-decisions: + - 'Removed UploadZone from EmptyState entirely (toolbar provides upload)' + - 'Added --color-text-muted and --color-text-dim CSS tokens for design accuracy' + +patterns-established: + - 'Box-drawing chars (U+250C/2500/2510/2502/2514/2518) for terminal window UI' + +# Metrics +duration: 2min +completed: 2026-02-07 +--- + +# Quick Task 002: Fix Empty State ASCII Art Summary + +Terminal-window ASCII art with box-drawing characters replacing basic folder icon, UploadZone removed from EmptyState. + +## Performance + +- **Duration:** 2 min +- **Started:** 2026-02-07T00:02:44Z +- **Completed:** 2026-02-07T00:04:43Z +- **Tasks:** 2 +- **Files modified:** 4 + +## Accomplishments + +- Replaced basic folder ASCII art with polished terminal-window using box-drawing characters +- Terminal shows `$ ls -la`, `total 0`, and `$ cursor-block` for authentic empty directory feel +- Removed redundant UploadZone from EmptyState (toolbar already handles uploads) +- Added design-accurate color tokens: ASCII art green (#00D084), label muted (#8b9a8f), hint dim (#4a5a4e) + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Add CSS color tokens and update empty state styles** - `99bc84d` (style) +2. **Task 2: Replace ASCII art and remove UploadZone from EmptyState** - `ff97f12` (feat) + +## Files Created/Modified + +- `apps/web/src/index.css` - Added --color-text-muted and --color-text-dim design tokens +- `apps/web/src/styles/file-browser.css` - Updated empty state colors, removed .empty-state-upload class +- `apps/web/src/components/file-browser/EmptyState.tsx` - Rewritten with terminal-window box-drawing art, no props, no UploadZone +- `apps/web/src/components/file-browser/FileBrowser.tsx` - Removed folderId prop from EmptyState render + +## Decisions Made + +- Removed UploadZone from EmptyState entirely since the toolbar already provides upload functionality +- Added two new CSS custom properties (--color-text-muted, --color-text-dim) rather than using inline colors + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- Empty state now matches the "EmptyState - Improved" design from the Pencil file +- Closes cosmetic UAT gap from Phase 6.3 + +--- + +_Quick Task: 002-fix-empty-state-ascii-art_ +_Completed: 2026-02-07_ diff --git a/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md b/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md index 6485f70591..dd1de52849 100644 --- a/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md +++ b/.planning/todos/pending/2026-01-22-atomic-file-upload-flow.md @@ -62,7 +62,7 @@ Optimize to a single atomic backend call that handles the entire upload flow: IPNS publish is external to the DB transaction — once published, cannot rollback: -``` +```text [DB Transaction] [External Call] ┌─────────────────────┐ ┌─────────────────┐ │ 1. Pin to IPFS │ │ │ @@ -83,26 +83,26 @@ IPNS publish is external to the DB transaction — once published, cannot rollba ### Options Evaluated -**Option A: Accept Partial State + Client Retry** +#### Option A: Accept Partial State + Client Retry - Pin → Record → Commit → Try IPNS - On IPNS failure: return `{ cid, ipnsError: true }`, client retries IPNS - Pros: Simple, file safely stored, client already has IPNS capability - Cons: Client retry logic, brief invisible window -**Option B: Optimistic IPNS (Publish First)** +#### Option B: Optimistic IPNS (Publish First) - Pin → Publish IPNS → Record → Commit - Pros: File always visible if exists - Cons: Worse failure mode (IPNS references non-existent file) -**Option C: Two-Phase with Pending State** +#### Option C: Two-Phase with Pending State - Record with `status: pending`, publish IPNS, then `status: confirmed` - Pros: Clear visibility, background retry - Cons: Complex state model, UI changes needed -**Option D: Idempotent Retry with Deduplication** +#### Option D: Idempotent Retry with Deduplication - Accept `{ expectedCid, idempotencyKey }`, skip completed steps on retry - Pros: True atomic from client view, safe retries @@ -140,6 +140,15 @@ IPNS publish is external to the DB transaction — once published, cannot rollba - Exact chunking algorithm match with Pinata for large files? - Should this be a new phase or quick task? +### CodeRabbit Review Notes (2026-02-07, PR #55) + +CodeRabbit independently identified the same per-file IPNS publish bottleneck: + +- **Call chain per file:** `addFile()` → `addFileToFolder()` → `updateFolderMetadata()` → `createAndPublishIpnsRecord()` — each triggering a separate IPNS publish (~2s each) +- **Suggested approach:** Create `addFilesToFolder` / `updateFolderMetadataBatch` function so `createAndPublishIpnsRecord` is invoked once after all files are added +- **Keep `addFile` for single-file use**, batch path calls `updateFolderMetadata` once at the end +- **Key files confirmed:** `EmptyState.tsx` (handleDrop loop), `useFileUpload.ts`, `upload.service.ts`, `folder.service.ts` (`addFileToFolder`, `updateFolderMetadata`) + --- -_Decision pending — return to this after Phase 7_ + diff --git a/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md b/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md new file mode 100644 index 0000000000..e14630e058 --- /dev/null +++ b/.planning/todos/pending/2026-02-07-web-worker-large-file-encryption.md @@ -0,0 +1,30 @@ +--- +created: 2026-02-07T12:00 +title: Offload large file encryption to Web Worker +area: ui +files: + - apps/web/src/services/file-crypto.service.ts + - apps/web/src/services/upload.service.ts +--- + +## Problem + +`encryptFile()` in `file-crypto.service.ts` calls `file.arrayBuffer()` and `encryptAesGcm()` on the main thread. For large files (approaching the 100MB limit), this blocks the UI thread and can cause the browser to appear frozen during encryption. + +Identified by CodeRabbit review on PR #55. + +## Solution + +Offload encryption to a Web Worker for files >= 10MB: + +1. Create a dedicated encryption worker (`encrypt.worker.ts`) +2. Worker receives `File`/`Blob`, generates file key + IV, runs `encryptAesGcm`, returns ciphertext + wrapped key +3. Main thread stays responsive, upload progress UI remains smooth +4. Files < 10MB continue using main thread (worker overhead not worth it for small files) + +**Considerations:** + +- Web Crypto API is available in Worker contexts +- Need to handle `Transferable` objects for zero-copy ArrayBuffer passing +- Worker bundling with Vite uses `new Worker(new URL(...), { type: 'module' })` pattern +- Error handling: worker crashes should fall back to main thread encryption diff --git a/apps/web/src/components/file-browser/EmptyState.tsx b/apps/web/src/components/file-browser/EmptyState.tsx index 96752549dd..a7146d464f 100644 --- a/apps/web/src/components/file-browser/EmptyState.tsx +++ b/apps/web/src/components/file-browser/EmptyState.tsx @@ -1,54 +1,120 @@ -import { UploadZone } from './UploadZone'; +import { useCallback, useState } from 'react'; +import { useDropzone, FileRejection } from 'react-dropzone'; +import { useFileUpload } from '../../hooks/useFileUpload'; +import { useFolder } from '../../hooks/useFolder'; -type EmptyStateProps = { - /** Folder ID for upload destination */ - folderId: string; -}; +const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB per FILE-01 /** - * Terminal-style ASCII art folder icon for empty state. + * Terminal-style ASCII art for empty state using box-drawing characters. + * Shows a mini terminal window with `ls -la` returning empty results. */ -const asciiArt = ` - ___________ - / /| - / / | -|__________| | -| | / -|__________|/ -`; +const terminalArt = `\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 +\u2502 $ ls -la \u2502 +\u2502 total 0 \u2502 +\u2502 $ \u2588 \u2502 +\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`; + +type EmptyStateProps = { + folderId: string; +}; -/** - * Empty state component shown when a folder has no contents. - * - * Displays terminal-style ASCII art with drop zone for uploads. - * Acts as both visual indicator and functional upload target. - * - * @example - * ```tsx - * function FileBrowser() { - * const { currentFolder, currentFolderId } = useFolderNavigation(); - * - * if (currentFolder?.children.length === 0) { - * return ; - * } - * - * return ; - * } - * ``` - */ export function EmptyState({ folderId }: EmptyStateProps) { + const { upload, canUpload, isUploading } = useFileUpload(); + const { addFile } = useFolder(); + const [error, setError] = useState(null); + + const handleDrop = useCallback( + async (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + setError(null); + + if (rejectedFiles.length > 0) { + const oversized = rejectedFiles.filter((r) => + r.errors.some((e) => e.code === 'file-too-large') + ); + if (oversized.length > 0) { + setError(`Files exceed 100MB limit: ${oversized.map((r) => r.file.name).join(', ')}`); + return; + } + setError('Some files were rejected'); + return; + } + + if (acceptedFiles.length === 0) return; + + const totalSize = acceptedFiles.reduce((sum, f) => sum + f.size, 0); + if (!canUpload(totalSize)) { + setError('Not enough storage space for these files'); + return; + } + + try { + const uploadedFiles = await upload(acceptedFiles); + const failedRegistrations: string[] = []; + for (const uploaded of uploadedFiles) { + try { + await addFile(folderId, { + cid: uploaded.cid, + wrappedKey: uploaded.wrappedKey, + iv: uploaded.iv, + originalName: uploaded.originalName, + originalSize: uploaded.originalSize, + }); + } catch { + failedRegistrations.push(uploaded.originalName ?? 'Unnamed file'); + } + } + if (failedRegistrations.length > 0) { + setError(`Some files uploaded but could not be added: ${failedRegistrations.join(', ')}`); + } + } catch (err) { + if ((err as Error).message !== 'Upload cancelled by user') { + setError((err as Error).message); + } + } + }, + [upload, canUpload, addFile, folderId] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop: handleDrop, + noClick: false, + multiple: true, + maxSize: MAX_FILE_SIZE, + }); + + const classes = ['empty-state']; + if (isDragActive) classes.push('empty-state-drag-active'); + if (isUploading) classes.push('empty-state-uploading'); + return ( -
+
+
-

// EMPTY DIRECTORY

-

drag files here or use upload

-
- -
+

+ {isUploading ? '// UPLOADING...' : isDragActive ? '// DROP FILES' : '// EMPTY DIRECTORY'} +

+

drag files here or use --upload

+ {error && ( +
+ {error} + +
+ )}
); } diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 853a97e28a..38e699b585 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -24,6 +24,8 @@ --color-border-dim: var(--color-green-darker); --color-text-primary: var(--color-green-primary); --color-text-secondary: var(--color-green-dim); + --color-text-muted: #8b9a8f; /* Muted gray-green for labels */ + --color-text-dim: #4a5a4e; /* Dim gray-green for hints */ /* Typography */ --font-family-mono: "JetBrains Mono", monospace; diff --git a/apps/web/src/styles/file-browser.css b/apps/web/src/styles/file-browser.css index fcdcfa8e3c..298216dadc 100644 --- a/apps/web/src/styles/file-browser.css +++ b/apps/web/src/styles/file-browser.css @@ -386,11 +386,17 @@ transition: border-color 0.2s ease, background-color 0.2s ease; } -.empty-state:hover { +.empty-state:hover, +.empty-state-drag-active { border-color: var(--color-green-primary); background-color: var(--color-green-darker); } +.empty-state-uploading { + opacity: 0.7; + pointer-events: none; +} + .empty-state-content { text-align: center; padding: var(--spacing-lg); @@ -401,7 +407,7 @@ font-family: var(--font-family-mono); font-size: var(--font-size-xs); line-height: 1.2; - color: var(--color-text-secondary); + color: var(--color-green-primary); margin: 0 0 var(--spacing-md) 0; white-space: pre; } @@ -418,19 +424,15 @@ font-family: var(--font-family-mono); font-size: var(--font-size-sm); margin: 0 0 var(--spacing-xs) 0; - color: var(--color-text-primary); + color: var(--color-text-muted); } .empty-state-hint { font-family: var(--font-family-mono); font-size: var(--font-size-xs); margin: 0; - color: var(--color-text-secondary); + color: var(--color-text-dim); text-transform: lowercase; } -.empty-state-upload { - margin-top: var(--spacing-md); -} - /* Note: Mobile responsive styles are in responsive.css */ diff --git a/designs/cipher-box-design.pen b/designs/cipher-box-design.pen index 2bab096021..f44ea4af90 100644 --- a/designs/cipher-box-design.pen +++ b/designs/cipher-box-design.pen @@ -1,5 +1,5 @@ { - "version": "2.6", + "version": "2.7", "children": [ { "type": "frame", @@ -6310,11 +6310,11 @@ { "type": "frame", "id": "xK9My", - "x": 700, + "x": 709, "y": 2207, "name": "Hover States", - "width": 500, - "height": 400, + "width": 491, + "height": 462, "fill": "#0a0a0a", "layout": "vertical", "gap": 24, @@ -6608,30 +6608,94 @@ }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "jX6oE", + "type": "frame", + "id": "KHYLS", "name": "rowNormalName", - "fill": "#00D084", - "content": "[DIR] documents/", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "normal" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lRG7s", + "name": "rowNormalIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "6HQvb", + "name": "rowNormalNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] }, { - "type": "text", - "id": "mKY3U", - "name": "rowNormalMeta", - "fill": "#006644", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "EwqEJ", + "name": "rowNormalSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "QgZZ5", + "name": "rowNormalSizeText", + "fill": "#006644", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "7hVtG", + "name": "rowNormalType", + "width": 120, + "children": [ + { + "type": "text", + "id": "2qmb9", + "name": "rowNormalTypeText", + "fill": "#006644", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ytkzi", + "name": "rowNormalModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "L8H03", + "name": "rowNormalModifiedText", + "fill": "#006644", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] }, @@ -6650,30 +6714,94 @@ }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "CjQb2", + "type": "frame", + "id": "woh7K", "name": "rowHoverName", - "fill": "#00D084", - "content": "[DIR] documents/ (hover)", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "normal" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kaD0M", + "name": "rowHoverIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "jhg9F", + "name": "rowHoverNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] }, { - "type": "text", - "id": "4Qxkv", - "name": "rowHoverMeta", - "fill": "#006644", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "IJVWm", + "name": "rowHoverSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "FLSs9", + "name": "rowHoverSizeText", + "fill": "#006644", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "69QUJ", + "name": "rowHoverType", + "width": 120, + "children": [ + { + "type": "text", + "id": "amV36", + "name": "rowHoverTypeText", + "fill": "#006644", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "InGuk", + "name": "rowHoverModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "GFAYF", + "name": "rowHoverModifiedText", + "fill": "#006644", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] }, @@ -6686,43 +6814,172 @@ "stroke": { "align": "inside", "thickness": { - "bottom": 1, - "left": 2 + "bottom": 1 }, - "fill": "#00D084" + "fill": "#003322" }, "padding": [ 10, - 0 + 16 ], "justifyContent": "space_between", "alignItems": "center", "children": [ { - "type": "text", - "id": "qhELR", + "type": "frame", + "id": "iVMjW", "name": "rowSelectedName", - "fill": "#00D084", - "content": "[DIR] documents/ (selected)", - "fontFamily": "JetBrains Mono", - "fontSize": 11, - "fontWeight": "600" + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c3nD3", + "name": "rowSelectedIcon", + "fill": "#00D084", + "content": "[DIR]", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "bJWKg", + "name": "rowSelectedNameText", + "fill": "#00D084", + "content": "documents", + "fontFamily": "JetBrains Mono", + "fontSize": 12, + "fontWeight": "600" + } + ] }, { - "type": "text", - "id": "WrRRp", - "name": "rowSelectedMeta", - "fill": "#00D084", - "content": "-- 30 Jan 2026", - "fontFamily": "JetBrains Mono", - "fontSize": 10, - "fontWeight": "normal" + "type": "frame", + "id": "bXb3Y", + "name": "rowSelectedSize", + "width": 120, + "children": [ + { + "type": "text", + "id": "TsuVh", + "name": "rowSelectedSizeText", + "fill": "#00D084", + "content": "--", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FU1cL", + "name": "rowSelectedType", + "width": 120, + "children": [ + { + "type": "text", + "id": "T44b5", + "name": "rowSelectedTypeText", + "fill": "#00D084", + "content": "directory", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "8Xksy", + "name": "rowSelectedModified", + "width": 180, + "children": [ + { + "type": "text", + "id": "zwdrl", + "name": "rowSelectedModifiedText", + "fill": "#00D084", + "content": "30 Jan 2026", + "fontFamily": "JetBrains Mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] } ] } ] } ] + }, + { + "type": "frame", + "id": "XyFqv", + "x": 0, + "y": 2769, + "name": "EmptyState - Improved", + "width": 600, + "height": 400, + "fill": "#000000", + "stroke": { + "align": "inside", + "thickness": 1, + "fill": "#00D084" + }, + "layout": "vertical", + "padding": 40, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CXeTR", + "name": "content", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Fswdw", + "name": "asciiArt", + "fill": "#00D084", + "content": " ┌──────────────────────┐\n │ $ ls -la │\n │ │\n │ total 0 │\n │ │\n │ $ █ │\n └──────────────────────┘", + "lineHeight": 1.3, + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vuc2z", + "name": "emptyText", + "fill": "#8b9a8f", + "content": "// EMPTY DIRECTORY", + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AGB2H", + "name": "hintText", + "fill": "#4a5a4e", + "content": "drag files here or use --upload", + "textAlign": "center", + "fontFamily": "Fira Code", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] } ] } \ No newline at end of file