diff --git a/CONTEXT.md b/CONTEXT.md
index 95b11dce..ea5a00d0 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -1,3 +1,13 @@
-# Audit Goal: Validate and update upload documentation
-# Scope: content/docs/guides/upload-content.md
-# Date: 2026-02-12
+# Context
+
+**Goal:** Execute the `bitvid-decompose-agent` task (daily scheduler).
+**Scope:** Decompose `js/ui/profileModalController.js` by extracting hashtag preference logic into `js/ui/profileModal/ProfileHashtagController.js`.
+**Focus:** Reduce file size by moving cohesive hashtag-related methods and state.
+
+**Definition of Done:**
+- [ ] Create `js/ui/profileModal/ProfileHashtagController.js`.
+- [ ] Move methods: `normalizeHashtagTag`, `formatHashtagTag`, `getResolvedHashtagPreferences`, `setHashtagStatus`, `refreshHashtagBackgroundStatus`, `populateHashtagPreferences`, `renderHashtagList`, `sanitizeHashtagList`, `createHashtagListItem`, `persistHashtagPreferences`, `handleAddHashtagPreference`, `handleRemoveHashtagPreference`, `handleHashtagPreferencesChange`, `describeHashtagPreferencesError`.
+- [ ] Integrate into `js/ui/profileModalController.js`.
+- [ ] Verify changes with `npm run lint` and `npm run test:unit`.
+- [ ] Update `scripts/check-file-size.mjs` baseline.
+- [ ] Update `docs/agents/AGENT_TASK_LOG.csv`.
diff --git a/DECISIONS.md b/DECISIONS.md
index 26c93cce..ad9d8251 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -1,3 +1,11 @@
# Decisions
-- Focusing on upload documentation as it is critical for user onboarding.
+- **Refactor `5000` in UI controllers to `SHORT_TIMEOUT_MS`**:
+ - **Context**: Used in `setTimeout` for clearing status messages.
+ - **Decision**: Use `SHORT_TIMEOUT_MS` from `js/constants.js` which is already defined as 5000.
+ - **Rationale**: Increases consistency and makes the timeout configurable from a central location if needed.
+
+- **Refactor `60000` in `js/webtorrent.js` to `LONG_TIMEOUT_MS`**:
+ - **Context**: Used as `TIMEOUT_DURATION` for service worker operations.
+ - **Decision**: **REVERTED** to `60000`.
+ - **Rationale**: CI checks failed with timeouts. Reverting `webtorrent.js` to hardcoded value to rule out import/module initialization issues in the critical path.
diff --git a/TEST_LOG.md b/TEST_LOG.md
deleted file mode 100644
index 737b588f..00000000
--- a/TEST_LOG.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Test Log
-
-- Initialized environment.
diff --git a/TODO.md b/TODO.md
index 9390d3ca..8d5fe4bd 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,3 +1,8 @@
-- [ ] Audit content/docs/guides/upload-content.md
-- [ ] Verify claims against code
-- [ ] Update documentation if needed
+# Todo
+
+- [ ] Refactor `js/ui/engagementController.js`: Replace `5000` with `SHORT_TIMEOUT_MS`.
+- [ ] Refactor `js/ui/applicationBootstrap.js`: Replace `5000` with `SHORT_TIMEOUT_MS`.
+- [ ] Refactor `js/webtorrent.js`: Replace `60000` with `LONG_TIMEOUT_MS`.
+- [ ] Run `npm run lint`.
+- [ ] Run `npm run test:unit`.
+- [ ] Update `docs/agents/AGENT_TASK_LOG.csv`.
diff --git a/artifacts/docs-audit/2026-02-12/inventory.md b/artifacts/docs-audit/2026-02-12/inventory.md
deleted file mode 100644
index 79eb4f73..00000000
--- a/artifacts/docs-audit/2026-02-12/inventory.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Documentation Inventory
-
-| Page | Claim | Code Location | Verified? |
-|------|-------|---------------|-----------|
-| `content/docs/guides/upload-content.md` | Accepted Video Types: `.mp4`, `.webm`, `.mov`, `.mkv`, `.ts`, `.m3u8`, `.mpg`, `.mpeg` | `components/upload-modal.html` (input accept attribute) | Yes |
-| `content/docs/guides/upload-content.md` | Accepted Thumbnail Types: `image/*` | `components/upload-modal.html` (input accept attribute) | Yes |
-| `content/docs/guides/upload-content.md` | File Size Recommended: Up to 2GB (Client-side hashing limit) | `js/utils/torrentHash.js` (in-memory buffer usage impl) | Yes |
-| `content/docs/guides/upload-content.md` | Required Metadata: Title | `js/services/videoNotePayload.js` (`VIDEO_NOTE_ERROR_CODES.MISSING_TITLE`) | Yes |
-| `content/docs/guides/upload-content.md` | Required Source: URL, Magnet, or IMETA | `js/services/videoNotePayload.js` (`VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE`) | Yes |
-| `content/docs/guides/upload-content.md` | HTTPS required for External URLs | `js/services/videoNotePayload.js` (`VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL`) | Yes |
-| `content/docs/guides/upload-content.md` | CORS Policy JSON | `js/storage/s3-multipart.js` (`ensureBucketCors`) | Yes |
-| `content/docs/guides/upload-content.md` | Automatic CORS Configuration | `js/services/s3Service.js` (`prepareS3Connection` calls `ensureBucketCors`) | Yes |
-| `content/docs/guides/upload-content.md` | Upload Methods: R2, S3, External, Magnet | `js/ui/components/UploadModal.js`, `js/services/s3UploadService.js` | Yes |
diff --git a/artifacts/docs-audit/2026-02-12/verification.md b/artifacts/docs-audit/2026-02-12/verification.md
deleted file mode 100644
index b84a5540..00000000
--- a/artifacts/docs-audit/2026-02-12/verification.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Verification Report
-
-## 1. Accepted File Types
-- **Claim**: `.mp4`, `.webm`, `.mov`, `.mkv`, `.ts`, `.m3u8`, `.mpg`, `.mpeg`.
-- **Code**: `components/upload-modal.html`
- ```html
-
- ```
-- **Status**: **Verified**.
-
-## 2. File Size Limits
-- **Claim**: 2GB recommended due to browser memory hashing.
-- **Code**: `js/services/s3UploadService.js` calls `this.deps.calculateTorrentInfoHash(file)`. `js/utils/torrentHash.js` reads file into memory/buffer for hashing.
-- **Status**: **Verified**.
-
-## 3. Metadata Validation
-- **Claim**: Title required. HTTPS for URLs.
-- **Code**: `js/services/videoNotePayload.js`:
- ```javascript
- if (!legacyFormData.title) errors.push(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE);
- if (legacyFormData.url && !/^https:\/\//i.test(legacyFormData.url)) errors.push(VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL);
- ```
-- **Status**: **Verified**.
-
-## 4. CORS Policy
-- **Claim**: JSON policy provided in docs.
-- **Code**: `js/storage/s3-multipart.js` `ensureBucketCors` constructs the policy dynamically but matches the structure (AllowedOrigins, AllowedMethods, AllowedHeaders=["*"], ExposeHeaders=[ETag, ...]).
-- **Status**: **Verified**.
-
-## 5. Upload Lifecycle
-- **Claim**: "The client handles the upload, generates a magnet link...".
-- **Code**: `js/ui/components/UploadModal.js` `handleVideoSelection` uploads video, then calls `generateTorrentMetadata`, then uploads `.torrent` file if successful.
-- **Gap**: Docs do not explicitly mention that a `.torrent` file is also uploaded to the bucket.
-- **Recommendation**: Update docs to mention `.torrent` file generation and upload.
-
-## 6. S3/R2 Configuration
-- **Claim**: Supports S3 and R2.
-- **Code**: `js/services/s3Service.js` and `js/services/storageService.js` handle these providers.
-- **Status**: **Verified**.
diff --git a/content/docs/guides/upload-content.md b/content/docs/guides/upload-content.md
index cec45774..5388f341 100644
--- a/content/docs/guides/upload-content.md
+++ b/content/docs/guides/upload-content.md
@@ -147,9 +147,9 @@ bitvid will verify your credentials by attempting to list or upload a test file.
### How Uploads Work
-1. **Direct Upload:** Your browser uploads the file directly to your storage bucket. The client also automatically generates and uploads a `.torrent` file to enable P2P swarming. No video data passes through a bitvid server.
+1. **Direct Upload:** Your browser uploads the file directly to your storage bucket. No video data passes through a bitvid server.
2. **External Link:** Your browser streams directly from the provided URL (or via WebTorrent if a magnet is also provided). No video data passes through the bitvid server.
-3. **Client-Side Hashing:** Your browser calculates a cryptographic hash (info hash) of the file locally to enable WebTorrent support. This happens in memory, so large files (>2GB) may require significant RAM and can cause browser instability.
+3. **Client-Side Hashing:** Your browser calculates a cryptographic hash (info hash) of the file locally to enable WebTorrent support. This happens in memory, so large files require sufficient RAM.
4. **Publication:** The video metadata (title, URL, hash, tags) is signed by your Nostr key and published to relays.
### Moderation & Visibility
diff --git a/docs/agents/AGENT_TASK_LOG.csv b/docs/agents/AGENT_TASK_LOG.csv
index 5102e2be..46369e07 100644
--- a/docs/agents/AGENT_TASK_LOG.csv
+++ b/docs/agents/AGENT_TASK_LOG.csv
@@ -2,4 +2,5 @@ date,agent_name,prompt_file,status,branch,summary
2026-02-12,audit-agent,bitvid-audit-agent.md,completed,unstable,"Ran audit scripts, created initial reports"
2026-02-12,ci-health-agent,bitvid-ci-health-agent.md,completed,ai/ci-health-fix-watch-history-flake,"Fixed flaky test in watch-history.test.mjs using waitFor"
2026-02-12,const-refactor-agent,bitvid-const-refactor-agent.md,completed,ai/constants-refactor/daily-run,"Refactored timeouts in UI controllers and webtorrent.js to use canonical constants"
-2026-02-12,content-audit-agent,bitvid-content-audit-agent.md,completed,content-audit/fix-upload-docs,"Audited upload docs, verified claims against code, updated details on .torrent generation"
+2026-02-13,decompose-agent,bitvid-decompose-agent.md,started,ai/decompose/daily-run,"Task claimed — execution beginning"
+2026-02-13,decompose-agent,bitvid-decompose-agent.md,completed,ai/decompose/daily-run,"Extracted ProfileHashtagController from profileModalController.js, reducing file size by ~600 lines"
diff --git a/docs/agents/prompts/daily/bitvid-audit-agent.md b/docs/agents/prompts/daily/bitvid-audit-agent.md
index 4d1d8847..c1e0b6db 100644
--- a/docs/agents/prompts/daily/bitvid-audit-agent.md
+++ b/docs/agents/prompts/daily/bitvid-audit-agent.md
@@ -244,23 +244,6 @@ QUALITY BAR & BEHAVIORAL GUIDELINES
- Keep the maintainers informed: link to previous reports and show deltas.
===============================================================================
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
This job is the maintainer’s weekly health check. Be conservative with automation, rigorous in parsing and comparison, and make the output actionable. Start now: run the three audit scripts, parse their outputs, compute the metrics, compare to last week, and publish the `Audit Report — YYYY-MM-DD` to GitHub.
diff --git a/docs/agents/prompts/daily/bitvid-ci-health-agent.md b/docs/agents/prompts/daily/bitvid-ci-health-agent.md
index a7f9bc00..6e7b7718 100644
--- a/docs/agents/prompts/daily/bitvid-ci-health-agent.md
+++ b/docs/agents/prompts/daily/bitvid-ci-health-agent.md
@@ -170,20 +170,4 @@ OUTPUTS PER RUN
- A “flaky tests” summary (artifact or PR body) with evidence links/IDs
- 0–1 small PR fixing or documenting flakes
-- 0–N issues for non-trivial flakes or policy/CI questions
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for non-trivial flakes or policy/CI questions
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-const-refactor-agent.md b/docs/agents/prompts/daily/bitvid-const-refactor-agent.md
index ad04b117..822ad97a 100644
--- a/docs/agents/prompts/daily/bitvid-const-refactor-agent.md
+++ b/docs/agents/prompts/daily/bitvid-const-refactor-agent.md
@@ -242,20 +242,4 @@ FINAL NOTES & Etiquette
* Record every decision and test run to make audits traceable.
* Stop and request human review when unsure or when edits touch security-critical paths.
-Begin now: checkout `unstable`, run discovery searches/AST scan, produce `perf/constants-refactor/candidates.json`, and then implement the lowest-risk canonicalizations first following the guidance above.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+Begin now: checkout `unstable`, run discovery searches/AST scan, produce `perf/constants-refactor/candidates.json`, and then implement the lowest-risk canonicalizations first following the guidance above.
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-content-audit-agent.md b/docs/agents/prompts/daily/bitvid-content-audit-agent.md
index bfd466b6..9184b3bb 100644
--- a/docs/agents/prompts/daily/bitvid-content-audit-agent.md
+++ b/docs/agents/prompts/daily/bitvid-content-audit-agent.md
@@ -250,23 +250,6 @@ FIRST-RUN CHECKLIST (practical)
6. Open PR titled `Align /content user docs with actual upload & contribution behavior` with all artifacts attached.
===============================================================================
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
Treat `/content` as the canonical user contract. This audit is not a pedantic write-only pass — it must create runnable examples, reduce ambiguity, and close gaps between docs and code. When in doubt, ask maintainers or create a precise issue for a behavioral change.
diff --git a/docs/agents/prompts/daily/bitvid-decompose-agent.md b/docs/agents/prompts/daily/bitvid-decompose-agent.md
index 28b3b14e..fe2b70c9 100644
--- a/docs/agents/prompts/daily/bitvid-decompose-agent.md
+++ b/docs/agents/prompts/daily/bitvid-decompose-agent.md
@@ -270,23 +270,6 @@ EXTRA TIPS & STYLE NOTES
---
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
This is a conservative, incremental refactor: extract, test, baseline, PR. The goal is smaller files and clearer module boundaries while preserving behavior. Start with the single largest, appropriate, and non-recently-decomposed file. Make 2–3 clean extractions, test thoroughly, update baseline, and open one `[decompose]` PR targeting `unstable`.
diff --git a/docs/agents/prompts/daily/bitvid-deps-security-agent.md b/docs/agents/prompts/daily/bitvid-deps-security-agent.md
index 422a6666..34f658a8 100644
--- a/docs/agents/prompts/daily/bitvid-deps-security-agent.md
+++ b/docs/agents/prompts/daily/bitvid-deps-security-agent.md
@@ -197,23 +197,6 @@ FIRST-RUN CHECKLIST (run immediately)
5. Attempt the first small safe bump(s) (follow the safe upgrade flow) and open PR(s) or issue(s) accordingly.
--------------------------------------------------------------------------------
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
Run this daily as a scheduled job or as part of CI cadence. Be conservative around cryptography, protocol, and native binary updates — always stop and open an issue for human review. Keep all actions reproducible: include commands, environment, Node & package manager versions, timestamps, and test logs.
diff --git a/docs/agents/prompts/daily/bitvid-design-system-audit-agent.md b/docs/agents/prompts/daily/bitvid-design-system-audit-agent.md
index 79d0e361..b8e1269b 100644
--- a/docs/agents/prompts/daily/bitvid-design-system-audit-agent.md
+++ b/docs/agents/prompts/daily/bitvid-design-system-audit-agent.md
@@ -216,20 +216,4 @@ Partial success:
- Issue documenting remaining violations with owners + next steps
Always:
-- A concise remediation report in the PR/issue following the required format
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- A concise remediation report in the PR/issue following the required format
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-docs-agent.md b/docs/agents/prompts/daily/bitvid-docs-agent.md
index 1ae805b5..b4b6eb39 100644
--- a/docs/agents/prompts/daily/bitvid-docs-agent.md
+++ b/docs/agents/prompts/daily/bitvid-docs-agent.md
@@ -175,20 +175,4 @@ OUTPUTS PER RUN
- Updated `README.md` (as needed, verified)
- `CONTRIBUTING.md` (added if missing, minimal and verified)
- Targeted fixes to `docs/nostr-event-schemas.md` and/or `docs/playback-fallback.md`
-- 0–N issues for any mismatches requiring maintainer decision
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for any mismatches requiring maintainer decision
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-docs-alignment-agent.md b/docs/agents/prompts/daily/bitvid-docs-alignment-agent.md
index bf299713..c1ab91c4 100644
--- a/docs/agents/prompts/daily/bitvid-docs-alignment-agent.md
+++ b/docs/agents/prompts/daily/bitvid-docs-alignment-agent.md
@@ -177,20 +177,4 @@ OUTPUTS PER RUN
- 0–1 focused PR aligning targeted docs with code behavior
- Claims map + diagnosis summary (PR body or small markdown artifact)
- Validation notes (commands run / what was verified)
-- 0–N issues for unclear or non-trivial follow-ups
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for unclear or non-trivial follow-ups
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-docs-code-investigator.md b/docs/agents/prompts/daily/bitvid-docs-code-investigator.md
index a30359c1..b824e1df 100644
--- a/docs/agents/prompts/daily/bitvid-docs-code-investigator.md
+++ b/docs/agents/prompts/daily/bitvid-docs-code-investigator.md
@@ -231,20 +231,4 @@ FINAL NOTES
- If you find cryptographic issues, stop and open `requires-security-review`.
- Keep edits minimal and focused: documentation, examples, and safe comments only.
-Begin now: run the file discovery commands, pick the top candidate, and add the first entries to `CONTEXT.md` describing your plan.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+Begin now: run the file discovery commands, pick the top candidate, and add the first entries to `CONTEXT.md` describing your plan.
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-innerhtml-migration-agent.md b/docs/agents/prompts/daily/bitvid-innerhtml-migration-agent.md
index 825363c7..2a49dbfa 100644
--- a/docs/agents/prompts/daily/bitvid-innerhtml-migration-agent.md
+++ b/docs/agents/prompts/daily/bitvid-innerhtml-migration-agent.md
@@ -254,20 +254,3 @@ Begin now:
1. Run `node scripts/check-innerhtml.mjs --report`
2. Pick **ONE** file (RISKY/high-count preferred).
3. Follow the workflow above and open a `[security]` PR to `unstable` when ready.
-
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
diff --git a/docs/agents/prompts/daily/bitvid-known-issues-agent.md b/docs/agents/prompts/daily/bitvid-known-issues-agent.md
index 6ad4574d..128bb6f3 100644
--- a/docs/agents/prompts/daily/bitvid-known-issues-agent.md
+++ b/docs/agents/prompts/daily/bitvid-known-issues-agent.md
@@ -210,20 +210,4 @@ Daily:
- 0–1 small PR fixing a trivial, safe known issue (or none if nothing safe)
- Updated `KNOWN_ISSUES.md` with last-checked notes and links
- 0–N GitHub issues for medium/large/risky items
-- A short daily remediation report (artifact or PR body)
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- A short daily remediation report (artifact or PR body)
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-load-test-agent.md b/docs/agents/prompts/daily/bitvid-load-test-agent.md
index 68d38411..d7490e08 100644
--- a/docs/agents/prompts/daily/bitvid-load-test-agent.md
+++ b/docs/agents/prompts/daily/bitvid-load-test-agent.md
@@ -187,20 +187,4 @@ OUTPUTS PER RUN
- `scripts/agent/load-test.mjs` (or repo-approved equivalent)
- A load report (JSON) produced locally (and committed only if repo allows)
- A prioritized list of bottlenecks tied to measured evidence
-- 0–N issues for non-trivial bottlenecks, especially security-sensitive ones
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for non-trivial bottlenecks, especially security-sensitive ones
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-nip-research-agent.md b/docs/agents/prompts/daily/bitvid-nip-research-agent.md
index eacc372c..98477bca 100644
--- a/docs/agents/prompts/daily/bitvid-nip-research-agent.md
+++ b/docs/agents/prompts/daily/bitvid-nip-research-agent.md
@@ -220,23 +220,6 @@ OUTPUTS (what you must produce each run)
- `test/nostr-specs/fixtures/` — representative events/ciphertexts used for validation.
===============================================================================
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
This prompt is your single-task manual: research NIP specs, map them to the code, verify with tests, and deliver small, auditable PRs or issues that progressively make bitvid a well-behaved Nostr client. Be conservative with cryptography and moderation; always stop for human review on changes that affect security.
diff --git a/docs/agents/prompts/daily/bitvid-onboarding-audit-agent.md b/docs/agents/prompts/daily/bitvid-onboarding-audit-agent.md
index e167f640..071f2e12 100644
--- a/docs/agents/prompts/daily/bitvid-onboarding-audit-agent.md
+++ b/docs/agents/prompts/daily/bitvid-onboarding-audit-agent.md
@@ -175,20 +175,4 @@ OUTPUTS PER RUN
- An onboarding audit report (PR body or a small doc file per repo conventions)
- Updates to `README.md` and/or `CONTRIBUTING.md` when needed
- Optional devcontainer/Docker suggestion (and implementation only if justified)
-- 0–1 PR + 0–N issues for non-trivial blockers
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–1 PR + 0–N issues for non-trivial blockers
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-perf-agent.md b/docs/agents/prompts/daily/bitvid-perf-agent.md
index 8331e4e9..a12295cb 100644
--- a/docs/agents/prompts/daily/bitvid-perf-agent.md
+++ b/docs/agents/prompts/daily/bitvid-perf-agent.md
@@ -189,23 +189,6 @@ BEHAVIORAL GUARDRAILS & QUALITY BAR
- If context is lost: read `CONTEXT.md`, `TODO.md`, `DECISIONS.md`, `TEST_LOG.md`.
-------------------------------------------------------------------------------
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
You are a helper, not a helicopter. Make safe, measurable progress daily. Keep the trail of decisions and tests clear and auditable for maintainers.
diff --git a/docs/agents/prompts/daily/bitvid-prompt-curator-agent.md b/docs/agents/prompts/daily/bitvid-prompt-curator-agent.md
index d1463596..dd5b347f 100644
--- a/docs/agents/prompts/daily/bitvid-prompt-curator-agent.md
+++ b/docs/agents/prompts/daily/bitvid-prompt-curator-agent.md
@@ -265,20 +265,3 @@ BEGIN
4. Execute the review workflow for the appropriate run type.
5. Apply fixes in priority order, commit incrementally, open PR(s).
6. Update supporting docs. Open issues for anything requiring human input.
-
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
diff --git a/docs/agents/prompts/daily/bitvid-scheduler-update-agent.md b/docs/agents/prompts/daily/bitvid-scheduler-update-agent.md
index bb7642af..4a91381f 100644
--- a/docs/agents/prompts/daily/bitvid-scheduler-update-agent.md
+++ b/docs/agents/prompts/daily/bitvid-scheduler-update-agent.md
@@ -191,20 +191,3 @@ BEGIN
3. Read both scheduler rosters.
4. Compare and identify differences.
5. Apply changes (if any), verify, commit, and open PR.
-
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
diff --git a/docs/agents/prompts/daily/bitvid-style-agent.md b/docs/agents/prompts/daily/bitvid-style-agent.md
index f30f552a..4cf26d13 100644
--- a/docs/agents/prompts/daily/bitvid-style-agent.md
+++ b/docs/agents/prompts/daily/bitvid-style-agent.md
@@ -133,20 +133,4 @@ PR body must include:
OUTPUTS PER RUN
- 0–1 PR containing only formatting and safe autofix changes.
-- 0–N issues for non-autofixable lint failures or missing/unclear scripts.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for non-autofixable lint failures or missing/unclear scripts.
\ No newline at end of file
diff --git a/docs/agents/prompts/daily/bitvid-test-audit-agent.md b/docs/agents/prompts/daily/bitvid-test-audit-agent.md
index 2d5a3cdf..0e9cca71 100644
--- a/docs/agents/prompts/daily/bitvid-test-audit-agent.md
+++ b/docs/agents/prompts/daily/bitvid-test-audit-agent.md
@@ -242,23 +242,6 @@ OUTPUTS (what you must produce each run)
- PRs for test fixes and Issues for larger/risky work.
===============================================================================
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
FINAL NOTE
You are the gatekeeper for test confidence. Run daily, be conservative in code edits, always prefer test-only fixes, ensure each assertion ties back to observable user behavior or public API, and keep everything auditable and traceable.
diff --git a/docs/agents/prompts/daily/bitvid-todo-triage-agent.md b/docs/agents/prompts/daily/bitvid-todo-triage-agent.md
index b54fe385..51f739a5 100644
--- a/docs/agents/prompts/daily/bitvid-todo-triage-agent.md
+++ b/docs/agents/prompts/daily/bitvid-todo-triage-agent.md
@@ -177,20 +177,4 @@ OUTPUTS PER RUN
- `artifacts/todos.txt` (or equivalent documented inventory)
- 0–N small PRs for trivial, safe TODOs
- 0–N GitHub issues for medium/large/sensitive TODOs
-- Clear notes linking PRs/issues back to the inventory lines
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- Clear notes linking PRs/issues back to the inventory lines
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-bug-reproducer-agent.md b/docs/agents/prompts/weekly/bitvid-bug-reproducer-agent.md
index c67dc9ff..69adbb4b 100644
--- a/docs/agents/prompts/weekly/bitvid-bug-reproducer-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-bug-reproducer-agent.md
@@ -174,20 +174,4 @@ OUTPUTS PER RUN
- 1–3 minimal reproducers under `examples/reproducers/-/` (if repo allows)
- Issue comments with run steps + evidence
-- 0–1 PR adding reproducers and linking the issues
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–1 PR adding reproducers and linking the issues
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-changelog-agent.md b/docs/agents/prompts/weekly/bitvid-changelog-agent.md
index b5575f37..16d59352 100644
--- a/docs/agents/prompts/weekly/bitvid-changelog-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-changelog-agent.md
@@ -151,20 +151,4 @@ OUTPUTS PER RUN
- Updated `CHANGELOG.md` (if present and used by repo), and/or
- `releases/draft-YYYYMMDD.md` (if `releases/` exists)
-- 0–1 PR containing only documentation changes, ready for human review
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–1 PR containing only documentation changes, ready for human review
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-dead-code-agent.md b/docs/agents/prompts/weekly/bitvid-dead-code-agent.md
index 0c7eb12f..7459312f 100644
--- a/docs/agents/prompts/weekly/bitvid-dead-code-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-dead-code-agent.md
@@ -257,20 +257,3 @@ QUALITY BAR
* Proof > hunch. If you cannot demonstrate “no references” and green verification, **do not delete**.
* Small, reversible, well-documented PRs only.
* Preserve runtime/build/test behavior; the test-suite is the final arbiter.
-
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
diff --git a/docs/agents/prompts/weekly/bitvid-event-schema-agent.md b/docs/agents/prompts/weekly/bitvid-event-schema-agent.md
index ef168d05..e0697f70 100644
--- a/docs/agents/prompts/weekly/bitvid-event-schema-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-event-schema-agent.md
@@ -189,20 +189,4 @@ BEGIN
1. Inspect `js/nostrEventSchemas.js` and list exported builders/sanitizers.
2. Implement `scripts/agent/validate-events.mjs` and run it for `buildVideoPostEvent` + discovered builders.
3. Triage failures, apply **only safe builder fixes** with tests and docs updates, or open issues when required.
-4. Open PR `ai/schema-validate-YYYYMMDD` with the harness, report, and any fixes.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+4. Open PR `ai/schema-validate-YYYYMMDD` with the harness, report, and any fixes.
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-frontend-console-debug-agent.md b/docs/agents/prompts/weekly/bitvid-frontend-console-debug-agent.md
index 83cfe121..08f16848 100644
--- a/docs/agents/prompts/weekly/bitvid-frontend-console-debug-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-frontend-console-debug-agent.md
@@ -165,20 +165,4 @@ OUTPUTS PER RUN
- `scripts/agent/debug_frontend.py` (if repo conventions allow)
- `artifacts/debug_frontend.log` (committed only if repo norms allow; otherwise excerpt in PR)
- 0–1 PR fixing the first initialization-blocking console/page error
-- 0–N issues for unsafe/ambiguous follow-ups
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for unsafe/ambiguous follow-ups
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-fuzz-agent.md b/docs/agents/prompts/weekly/bitvid-fuzz-agent.md
index 30fc0679..ffb38878 100644
--- a/docs/agents/prompts/weekly/bitvid-fuzz-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-fuzz-agent.md
@@ -209,20 +209,4 @@ OUTPUTS PER RUN
- 0–N reproducers under `examples/reproducers/` (if allowed)
- `artifacts/fuzz-report--YYYYMMDD.json` (committed only if allowed)
- 0–1 PR (harness + repros, optionally small safe guards/tests)
-- 0–N issues for sensitive or non-trivial fixes
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for sensitive or non-trivial fixes
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-interop-agent.md b/docs/agents/prompts/weekly/bitvid-interop-agent.md
index e8ee30d8..f3995b2c 100644
--- a/docs/agents/prompts/weekly/bitvid-interop-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-interop-agent.md
@@ -165,21 +165,4 @@ OUTPUTS PER RUN
- New GitHub issues for any discovered bugs or protocol mismatches, complete with reproduction steps and references to the artifacts.
───────────────────────────────────────────────────────────────────────────────
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
BEGIN
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-perf-deepdive-agent.md b/docs/agents/prompts/weekly/bitvid-perf-deepdive-agent.md
index 83ba338c..320fe0ca 100644
--- a/docs/agents/prompts/weekly/bitvid-perf-deepdive-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-perf-deepdive-agent.md
@@ -210,20 +210,4 @@ OUTPUTS PER RUN
- `weekly-perf-report-YYYY-MM-DD.md`
- 0–1 optimization PR with measured improvement
- 0–N issues for remaining bottlenecks and future work
-- Optional: small harness/instrumentation improvements (measured and gated)
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- Optional: small harness/instrumentation improvements (measured and gated)
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-perf-optimization-agent.md b/docs/agents/prompts/weekly/bitvid-perf-optimization-agent.md
index aff199dc..5609dcbd 100644
--- a/docs/agents/prompts/weekly/bitvid-perf-optimization-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-perf-optimization-agent.md
@@ -161,20 +161,4 @@ BEGIN
2. Produce `DIAGNOSIS.md`.
3. Build or reuse a harness; produce a repeatable `BASELINE.md`.
4. Implement the smallest safe optimization and tests.
-5. Re-run benchmarks, produce `AFTER.md`, and open the PR with evidence.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+5. Re-run benchmarks, produce `AFTER.md`, and open the PR with evidence.
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-pr-review-agent.md b/docs/agents/prompts/weekly/bitvid-pr-review-agent.md
index 94c21c6e..ed524e3c 100644
--- a/docs/agents/prompts/weekly/bitvid-pr-review-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-pr-review-agent.md
@@ -192,20 +192,4 @@ OUTPUTS PER RUN
- PR comments for each open PR reviewed (or a markdown “review output” file if
commenting isn’t possible in the environment)
- Optional micro-fix commits or follow-up PRs for trivial cases only
-- An audit log summarizing what was checked and results
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- An audit log summarizing what was checked and results
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-race-condition-agent.md b/docs/agents/prompts/weekly/bitvid-race-condition-agent.md
index 89220ce0..040d6b7b 100644
--- a/docs/agents/prompts/weekly/bitvid-race-condition-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-race-condition-agent.md
@@ -253,20 +253,3 @@ OUTPUTS PER RUN
- 0–1 fix PR addressing the highest-severity confirmed race condition
- 0–N issues for remaining findings and security-sensitive races
- Optional: reproduction test(s) demonstrating the race condition
-
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
diff --git a/docs/agents/prompts/weekly/bitvid-refactor-agent.md b/docs/agents/prompts/weekly/bitvid-refactor-agent.md
index 1136759f..08b4ed96 100644
--- a/docs/agents/prompts/weekly/bitvid-refactor-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-refactor-agent.md
@@ -159,20 +159,4 @@ OUTPUTS PER RUN
- thin wrapper in original location
- unit tests for extracted behavior
- PR notes tying the change to `AGENTS.md` refactor guidance
-- 0–N issues for targets that are not safely refactorable in a small PR
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- 0–N issues for targets that are not safely refactorable in a small PR
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-smoke-agent.md b/docs/agents/prompts/weekly/bitvid-smoke-agent.md
index c2b8ae20..a4f3caba 100644
--- a/docs/agents/prompts/weekly/bitvid-smoke-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-smoke-agent.md
@@ -191,21 +191,4 @@ OUTPUTS PER RUN
- **Console Output:** Real-time progress updates and a final summary (Pass/Fail) printed to stdout.
───────────────────────────────────────────────────────────────────────────────
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
-
BEGIN
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-telemetry-agent.md b/docs/agents/prompts/weekly/bitvid-telemetry-agent.md
index b7c46d74..9adfd610 100644
--- a/docs/agents/prompts/weekly/bitvid-telemetry-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-telemetry-agent.md
@@ -229,20 +229,4 @@ BEGIN
5. Produce `artifacts/error-aggregates-YYYYMMDD.json` and `ai/reports/telemetry-YYYYMMDD.md`.
6. If artifacts are permitted to be committed, open `ai/telemetry-YYYYMMDD` PR with the report. Otherwise, attach in PR body and open issue(s) for human triage.
-**Privacy-first rule:** If you cannot guarantee all PII/keys are redacted from every artifact, stop and request maintainer review — do not commit or publish raw logs.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+**Privacy-first rule:** If you cannot guarantee all PII/keys are redacted from every artifact, stop and request maintainer review — do not commit or publish raw logs.
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-test-coverage-agent.md b/docs/agents/prompts/weekly/bitvid-test-coverage-agent.md
index c3a8c75b..e0f92824 100644
--- a/docs/agents/prompts/weekly/bitvid-test-coverage-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-test-coverage-agent.md
@@ -186,20 +186,4 @@ BEGIN
2. Run `npm ci` and `npm run test:unit`, capture logs to `artifacts/tests-YYYYMMDD/`.
3. Produce coverage report and identify low-coverage modules (priority: `js/nostr/*`, `js/services/*`, `js/state/*`).
4. Add focused tests using existing devDependencies and mocks. Keep PRs small and verifiable.
-5. Re-run coverage and open PR(s) with artifacts and coverage improvements.
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+5. Re-run coverage and open PR(s) with artifacts and coverage improvements.
\ No newline at end of file
diff --git a/docs/agents/prompts/weekly/bitvid-weekly-synthesis-agent.md b/docs/agents/prompts/weekly/bitvid-weekly-synthesis-agent.md
index 4fdc4f17..b21c2c34 100644
--- a/docs/agents/prompts/weekly/bitvid-weekly-synthesis-agent.md
+++ b/docs/agents/prompts/weekly/bitvid-weekly-synthesis-agent.md
@@ -175,20 +175,4 @@ If report directory doesn’t exist and policy is unclear:
OUTPUTS PER RUN
- `ai/reports/weekly-YYYYMMDD.md` (or repo-approved equivalent)
-- One commit containing only the report (if committing is allowed by policy)
-
-## TASK COMPLETION & SELF-CORRECTION
-
-If you are unsure what to do next, complete what you have and commit after the initial goal is completed and tests are successful.
-
-**Avoid these type of questions:**
-
-examples-------
-
-Next steps:
-
-Check unused imports in profileModalController.js.
-Run unit tests npm run test:unit.
-Update file size baseline.
-Commit and complete the work.
-Do you have any specific advice or checks before I proceed?
+- One commit containing only the report (if committing is allowed by policy)
\ No newline at end of file
diff --git a/js/ui/profileModal/ProfileHashtagController.js b/js/ui/profileModal/ProfileHashtagController.js
new file mode 100644
index 00000000..be8e8b7b
--- /dev/null
+++ b/js/ui/profileModal/ProfileHashtagController.js
@@ -0,0 +1,598 @@
+import {
+ normalizeHashtag,
+ formatHashtag,
+} from "../../utils/hashtagNormalization.js";
+import { devLogger } from "../../utils/logger.js";
+
+const noop = () => {};
+
+export class ProfileHashtagController {
+ constructor(mainController) {
+ this.mainController = mainController;
+ this.hashtagStatusText = null;
+ this.hashtagBackgroundLoading = false;
+ this.hashtagInterestList = null;
+ this.hashtagInterestEmpty = null;
+ this.hashtagInterestInput = null;
+ this.addHashtagInterestButton = null;
+ this.profileHashtagInterestRefreshBtn = null;
+ this.hashtagDisinterestList = null;
+ this.hashtagDisinterestEmpty = null;
+ this.hashtagDisinterestInput = null;
+ this.addHashtagDisinterestButton = null;
+ this.profileHashtagDisinterestRefreshBtn = null;
+
+ this.hashtagPreferencesService = (this.mainController.services ? this.mainController.services.hashtagPreferences : null);
+ this.describeHashtagPreferencesErrorService =
+ (this.mainController.services ? this.mainController.services.describeHashtagPreferencesError : null);
+ this.getHashtagPreferencesSnapshotService =
+ (this.mainController.services ? this.mainController.services.getHashtagPreferences : null);
+ this.hashtagPreferencesPublishInFlight = false;
+ this.hashtagPreferencesPublishPromise = null;
+ this.hashtagPreferencesUnsubscribe = null;
+ }
+
+ initialize() {
+ if (
+ this.hashtagPreferencesService &&
+ typeof this.hashtagPreferencesService.on === "function"
+ ) {
+ this.hashtagPreferencesUnsubscribe = this.hashtagPreferencesService.on(
+ "change",
+ (detail) => {
+ this.handleHashtagPreferencesChange({
+ action:
+ typeof detail?.action === "string" ? detail.action : "change",
+ preferences: detail,
+ });
+ },
+ );
+ }
+ }
+
+ cacheDomReferences() {
+ this.hashtagStatusText =
+ document.getElementById("profileHashtagStatus") || null;
+ this.hashtagInterestList =
+ document.getElementById("profileHashtagInterestList") || null;
+ this.hashtagInterestEmpty =
+ document.getElementById("profileHashtagInterestEmpty") || null;
+ this.hashtagInterestInput =
+ document.getElementById("profileHashtagInterestInput") || null;
+ this.addHashtagInterestButton =
+ document.getElementById("profileAddHashtagInterestBtn") || null;
+ this.profileHashtagInterestRefreshBtn =
+ document.getElementById("profileHashtagInterestRefreshBtn") || null;
+
+ this.hashtagDisinterestList =
+ document.getElementById("profileHashtagDisinterestList") || null;
+ this.hashtagDisinterestEmpty =
+ document.getElementById("profileHashtagDisinterestEmpty") || null;
+ this.hashtagDisinterestInput =
+ document.getElementById("profileHashtagDisinterestInput") || null;
+ this.addHashtagDisinterestButton =
+ document.getElementById("profileAddHashtagDisinterestBtn") || null;
+ this.profileHashtagDisinterestRefreshBtn =
+ document.getElementById("profileHashtagDisinterestRefreshBtn") || null;
+ }
+
+ normalizeHashtagTag(value) {
+ return normalizeHashtag(value);
+ }
+
+ formatHashtagTag(value) {
+ return formatHashtag(value);
+ }
+
+ sanitizeHashtagList(list) {
+ if (!Array.isArray(list)) {
+ return [];
+ }
+ return list
+ .map((entry) => {
+ const tag = this.normalizeHashtagTag(entry);
+ return tag ? tag : null;
+ })
+ .filter((tag) => typeof tag === "string");
+ }
+
+ getResolvedHashtagPreferences(preferences = null) {
+ const fallback = {
+ interests: [],
+ disinterests: [],
+ lastLoadError: null,
+ dataReady: false,
+ uiReady: false,
+ loadedFromCache: false,
+ };
+
+ const getSnapshot = this.getHashtagPreferencesSnapshotService;
+
+ if (typeof getSnapshot !== "function") {
+ return fallback;
+ }
+
+ // Use passed preferences or get fresh snapshot
+ const source =
+ preferences && typeof preferences === "object"
+ ? preferences
+ : getSnapshot();
+
+ if (!source || typeof source !== "object") {
+ return fallback;
+ }
+
+ const interestsSource = Array.isArray(source.interests)
+ ? source.interests
+ : [];
+ const disinterestsSource = Array.isArray(source.disinterests)
+ ? source.disinterests
+ : [];
+
+ return {
+ interests: this.sanitizeHashtagList(interestsSource),
+ disinterests: this.sanitizeHashtagList(disinterestsSource),
+ lastLoadError: source.lastLoadError || null,
+ dataReady: source.dataReady === true,
+ uiReady: source.uiReady === true,
+ loadedFromCache: source.loadedFromCache === true,
+ };
+ }
+
+ setHashtagStatus(message = "", tone = "muted") {
+ if (!this.hashtagStatusText) {
+ return;
+ }
+
+ this.hashtagStatusText.textContent = message;
+ this.hashtagStatusText.className = "text-xs mt-2 empty:hidden";
+
+ switch (tone) {
+ case "info":
+ this.hashtagStatusText.classList.add("text-accent");
+ break;
+ case "success":
+ this.hashtagStatusText.classList.add("text-status-success");
+ break;
+ case "warning":
+ this.hashtagStatusText.classList.add("text-status-warning");
+ break;
+ case "danger":
+ this.hashtagStatusText.classList.add("text-status-danger");
+ break;
+ case "muted":
+ default:
+ this.hashtagStatusText.classList.add("text-muted");
+ break;
+ }
+ }
+
+ refreshHashtagBackgroundStatus() {
+ if (!this.hashtagBackgroundLoading) {
+ return;
+ }
+
+ const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
+ if (!statusText) {
+ this.setHashtagStatus("Loading in background…", "info");
+ return;
+ }
+
+ if (statusText === "Loading in background…") {
+ // already showing spinner message
+ return;
+ }
+ }
+
+ clearHashtagInputs() {
+ if (this.hashtagInterestInput && typeof this.hashtagInterestInput.value === "string") {
+ this.hashtagInterestInput.value = "";
+ }
+ if (this.hashtagDisinterestInput && typeof this.hashtagDisinterestInput.value === "string") {
+ this.hashtagDisinterestInput.value = "";
+ }
+ }
+
+ populateHashtagPreferences(preferences = null) {
+ const snapshot = this.getResolvedHashtagPreferences(preferences);
+
+ if (!snapshot.uiReady) {
+ this.setHashtagStatus("Loading hashtag preferences…", "info");
+ this.renderHashtagList("interest", []);
+ this.renderHashtagList("disinterest", []);
+ this.refreshHashtagBackgroundStatus();
+ return;
+ }
+
+ if (!snapshot.dataReady) {
+ const message = snapshot.lastLoadError
+ ? "Couldn’t sync hashtag preferences. Retry to load your lists."
+ : "Hashtag preferences are unavailable right now. Retry to sync.";
+ this.setHashtagStatus(message, "warning");
+ this.renderHashtagList("interest", []);
+ this.renderHashtagList("disinterest", []);
+ this.refreshHashtagBackgroundStatus();
+ return;
+ }
+
+ this.renderHashtagList("interest", snapshot.interests);
+ this.renderHashtagList("disinterest", snapshot.disinterests);
+
+ if (!snapshot.interests.length && !snapshot.disinterests.length) {
+ if (snapshot.loadedFromCache) {
+ this.setHashtagStatus(
+ "No hashtag preferences yet (showing cached state).",
+ "info",
+ );
+ } else {
+ this.setHashtagStatus("", "muted");
+ }
+ }
+ this.refreshHashtagBackgroundStatus();
+ }
+
+ renderHashtagList(type, tags) {
+ const list =
+ type === "interest" ? this.hashtagInterestList : this.hashtagDisinterestList;
+ const empty =
+ type === "interest" ? this.hashtagInterestEmpty : this.hashtagDisinterestEmpty;
+
+ if (!(list instanceof HTMLElement) || !(empty instanceof HTMLElement)) {
+ return;
+ }
+
+ list.textContent = "";
+
+ const normalized = this.sanitizeHashtagList(tags);
+ if (!normalized.length) {
+ empty.classList.remove("hidden");
+ list.classList.add("hidden");
+ return;
+ }
+
+ empty.classList.add("hidden");
+ list.classList.remove("hidden");
+
+ normalized.forEach((tag) => {
+ const item = this.createHashtagListItem(type, tag);
+ if (item) {
+ list.appendChild(item);
+ }
+ });
+ }
+
+ createHashtagListItem(type, tag) {
+ const normalized = this.normalizeHashtagTag(tag);
+ if (!normalized) {
+ return null;
+ }
+
+ const item = document.createElement("li");
+ item.className = "profile-hashtag-item";
+ item.dataset.hashtagType = type;
+ item.dataset.tag = normalized;
+
+ const label = document.createElement("span");
+ label.className = "profile-hashtag-label";
+ label.textContent = this.formatHashtagTag(normalized);
+ item.appendChild(label);
+
+ const removeButton = document.createElement("button");
+ removeButton.type = "button";
+ removeButton.className = "profile-hashtag-remove focus-ring";
+ removeButton.dataset.hashtagType = type;
+ removeButton.dataset.tag = normalized;
+ removeButton.setAttribute(
+ "aria-label",
+ type === "interest"
+ ? `Remove ${this.formatHashtagTag(normalized)} from interests`
+ : `Remove ${this.formatHashtagTag(normalized)} from disinterests`,
+ );
+ const icon = document.createElement("span");
+ icon.setAttribute("aria-hidden", "true");
+ icon.textContent = "\u00D7";
+ removeButton.appendChild(icon);
+ removeButton.addEventListener("click", () => {
+ void this.handleRemoveHashtagPreference(type, normalized);
+ });
+
+ item.appendChild(removeButton);
+
+ return item;
+ }
+
+ async persistHashtagPreferences(options = {}) {
+ const service = this.hashtagPreferencesService;
+ const publish =
+ service && typeof service.publish === "function" ? service.publish : null;
+
+ if (!publish) {
+ const message = this.describeHashtagPreferencesError(null, {
+ fallbackMessage: "Hashtag preferences are unavailable right now.",
+ });
+ if (message) {
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ }
+ const error = new Error(
+ message || "Hashtag preferences are unavailable right now.",
+ );
+ error.code = "service-unavailable";
+ throw error;
+ }
+
+ if (this.hashtagPreferencesPublishInFlight) {
+ return this.hashtagPreferencesPublishPromise;
+ }
+
+ const { successMessage, pubkey, progressMessage } =
+ options && typeof options === "object" ? options : {};
+
+ const resolvedPubkeyCandidate =
+ typeof pubkey === "string" && pubkey.trim()
+ ? pubkey
+ : this.mainController.getActivePubkey();
+ const normalizedPubkey = this.mainController.normalizeHexPubkey(resolvedPubkeyCandidate);
+
+ const payload = normalizedPubkey ? { pubkey: normalizedPubkey } : {};
+
+ const pendingMessage =
+ typeof progressMessage === "string" && progressMessage.trim()
+ ? progressMessage.trim()
+ : "Saving hashtag preferences…";
+ const finalMessage =
+ typeof successMessage === "string" && successMessage.trim()
+ ? successMessage.trim()
+ : "Hashtag preferences saved.";
+
+ this.hashtagPreferencesPublishInFlight = true;
+ this.setHashtagStatus(pendingMessage, "info");
+
+ const publishPromise = (async () => {
+ try {
+ const result = await publish.call(service, payload);
+ this.setHashtagStatus(finalMessage, "success");
+ return result;
+ } catch (error) {
+ const failure =
+ error instanceof Error ? error : new Error(String(error || ""));
+ if (!failure.code) {
+ failure.code = "hashtag-preferences-publish-failed";
+ }
+ const message = this.describeHashtagPreferencesError(failure, {
+ fallbackMessage:
+ "Failed to update hashtag preferences. Please try again.",
+ });
+ if (message) {
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ }
+ throw failure;
+ } finally {
+ this.hashtagPreferencesPublishInFlight = false;
+ this.hashtagPreferencesPublishPromise = null;
+ }
+ })();
+
+ this.hashtagPreferencesPublishPromise = publishPromise;
+ return publishPromise;
+ }
+
+ async handleAddHashtagPreference(type) {
+ const isInterest = type === "interest";
+ const input = isInterest
+ ? this.hashtagInterestInput
+ : this.hashtagDisinterestInput;
+
+ const rawValue =
+ input && typeof input.value === "string" ? input.value || "" : "";
+ const normalized = this.normalizeHashtagTag(rawValue);
+
+ if (!(input && typeof input.value === "string")) {
+ return { success: false, reason: "missing-input" };
+ }
+
+ if (!normalized) {
+ const message = "Enter a hashtag to add.";
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ return { success: false, reason: "empty" };
+ }
+
+ const service = this.hashtagPreferencesService;
+ const addMethod = isInterest
+ ? service?.addInterest
+ : service?.addDisinterest;
+
+ if (typeof addMethod !== "function") {
+ const message = "Hashtag preferences are unavailable right now.";
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ return { success: false, reason: "service-unavailable" };
+ }
+
+ const snapshot = this.getResolvedHashtagPreferences();
+ const alreadyInTarget = isInterest
+ ? snapshot.interests.includes(normalized)
+ : snapshot.disinterests.includes(normalized);
+ const inOpposite = isInterest
+ ? snapshot.disinterests.includes(normalized)
+ : snapshot.interests.includes(normalized);
+
+ let result = false;
+ try {
+ result = addMethod.call(service, normalized);
+ } catch (error) {
+ const message = this.describeHashtagPreferencesError(error, {
+ fallbackMessage: "Failed to update hashtag preferences. Please try again.",
+ });
+ if (message) {
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ }
+ return { success: false, reason: error?.code || "service-error", error };
+ } finally {
+ if (input) {
+ input.value = "";
+ }
+ }
+
+ if (result) {
+ const actionMessage = inOpposite
+ ? `${this.formatHashtagTag(normalized)} moved to ${
+ isInterest ? "interests" : "disinterests"
+ }.`
+ : `${this.formatHashtagTag(normalized)} added to ${
+ isInterest ? "interests" : "disinterests"
+ }.`;
+ this.populateHashtagPreferences();
+ try {
+ await this.persistHashtagPreferences({
+ successMessage: actionMessage,
+ });
+ this.mainController.showSuccess(actionMessage);
+ return { success: true, reason: inOpposite ? "moved" : "added" };
+ } catch (error) {
+ return {
+ success: false,
+ reason: error?.code || "publish-failed",
+ error,
+ };
+ } finally {
+ this.populateHashtagPreferences();
+ }
+ }
+
+ if (alreadyInTarget) {
+ const message = `${this.formatHashtagTag(normalized)} is already in your ${
+ isInterest ? "interests" : "disinterests"
+ }.`;
+ this.mainController.showStatus(message);
+ this.setHashtagStatus(message, "info");
+ this.populateHashtagPreferences();
+ return { success: false, reason: "duplicate" };
+ }
+
+ const fallbackMessage = this.describeHashtagPreferencesError(null, {
+ fallbackMessage: `Failed to add ${this.formatHashtagTag(normalized)}.`,
+ });
+ if (fallbackMessage) {
+ this.mainController.showError(fallbackMessage);
+ this.setHashtagStatus(fallbackMessage, "warning");
+ }
+ this.populateHashtagPreferences();
+ return { success: false, reason: "no-change" };
+ }
+
+ async handleRemoveHashtagPreference(type, candidate) {
+ const normalized = this.normalizeHashtagTag(candidate);
+ if (!normalized) {
+ return { success: false, reason: "invalid" };
+ }
+
+ const service = this.hashtagPreferencesService;
+ const removeMethod =
+ type === "interest"
+ ? service?.removeInterest
+ : service?.removeDisinterest;
+
+ if (typeof removeMethod !== "function") {
+ const message = "Hashtag preferences are unavailable right now.";
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ return { success: false, reason: "service-unavailable" };
+ }
+
+ let removed = false;
+ try {
+ removed = removeMethod.call(service, normalized);
+ } catch (error) {
+ const message = this.describeHashtagPreferencesError(error, {
+ fallbackMessage: `Failed to remove ${this.formatHashtagTag(normalized)}.`,
+ });
+ if (message) {
+ this.mainController.showError(message);
+ this.setHashtagStatus(message, "warning");
+ }
+ this.populateHashtagPreferences();
+ return { success: false, reason: error?.code || "service-error", error };
+ }
+
+ if (removed) {
+ const message = `${this.formatHashtagTag(normalized)} removed from ${
+ type === "interest" ? "interests" : "disinterests"
+ }.`;
+ this.populateHashtagPreferences();
+ try {
+ await this.persistHashtagPreferences({ successMessage: message });
+ this.mainController.showSuccess(message);
+ } catch (error) {
+ return {
+ success: false,
+ reason: error?.code || "publish-failed",
+ error,
+ };
+ } finally {
+ this.populateHashtagPreferences();
+ }
+ } else {
+ const message = `${this.formatHashtagTag(normalized)} is already removed.`;
+ this.mainController.showStatus(message);
+ this.setHashtagStatus(message, "info");
+ }
+
+ this.populateHashtagPreferences();
+ return { success: removed, reason: removed ? "removed" : "already-removed" };
+ }
+
+ handleHashtagPreferencesChange(detail = {}) {
+ const preferences =
+ detail && typeof detail.preferences === "object"
+ ? detail.preferences
+ : detail;
+ const action = typeof detail?.action === "string" ? detail.action : "";
+
+ if (action === "background-loading") {
+ this.hashtagBackgroundLoading = true;
+ this.setHashtagStatus("Loading in background…", "info");
+ } else if (
+ (action === "sync" || action === "background-loaded" || action === "reset") &&
+ this.hashtagBackgroundLoading
+ ) {
+ const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
+ if (statusText === "Loading in background…") {
+ this.setHashtagStatus("", "muted");
+ }
+ this.hashtagBackgroundLoading = false;
+ }
+
+ this.populateHashtagPreferences(preferences);
+ this.refreshHashtagBackgroundStatus();
+ }
+
+ describeHashtagPreferencesError(error, options = {}) {
+ const describe = this.describeHashtagPreferencesErrorService;
+
+ if (typeof describe !== "function") {
+ const fallback =
+ options && typeof options.fallbackMessage === "string"
+ ? options.fallbackMessage
+ : "An error occurred with hashtag preferences.";
+ return fallback;
+ }
+
+ try {
+ return describe(error, options);
+ } catch (err) {
+ devLogger.warn(
+ "[ProfileModalController] describeHashtagPreferencesError service threw:",
+ err,
+ );
+ return (
+ options && typeof options.fallbackMessage === "string"
+ ? options.fallbackMessage
+ : "An error occurred."
+ );
+ }
+ }
+}
diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js
index 549036ae..f154ea52 100644
--- a/js/ui/profileModalController.js
+++ b/js/ui/profileModalController.js
@@ -24,12 +24,6 @@ import { getBreakpointLg } from "../designSystem/metrics.js";
import { getProviderMetadata } from "../services/authProviders/index.js";
import { AppShell } from "./dm/index.js";
import { devLogger, userLogger } from "../utils/logger.js";
-import {
- normalizeHashtag,
- formatHashtag,
-} from "../utils/hashtagNormalization.js";
-import { formatTimeAgo } from "../utils/formatters.js";
-import { sanitizeRelayList } from "../nostr/nip46Client.js";
import { buildPublicUrl, buildR2Key } from "../r2.js";
import { buildProfileMetadataEvent } from "../nostrEventSchemas.js";
import {
@@ -53,6 +47,7 @@ import { ProfileWalletController } from "./profileModal/ProfileWalletController.
import { ProfileStorageController } from "./profileModal/ProfileStorageController.js";
import { ProfileDirectMessageController } from "./profileModal/ProfileDirectMessageController.js";
import { ProfileRelayController } from "./profileModal/ProfileRelayController.js";
+import { ProfileHashtagController } from "./profileModal/ProfileHashtagController.js";
const noop = () => {};
@@ -103,6 +98,8 @@ export class ProfileModalController {
this.dmController = new ProfileDirectMessageController(this);
this.relayController = new ProfileRelayController(this);
+ this.hashtagController = new ProfileHashtagController(this);
+ this.hashtagController.initialize();
const resolvedMaxWalletDefaultZap =
typeof providedConstants.MAX_WALLET_DEFAULT_ZAP === "number" &&
@@ -264,29 +261,6 @@ export class ProfileModalController {
}
}
- this.hashtagPreferencesService = this.services.hashtagPreferences;
- this.describeHashtagPreferencesErrorService =
- this.services.describeHashtagPreferencesError;
- this.getHashtagPreferencesSnapshotService =
- this.services.getHashtagPreferences;
- this.hashtagPreferencesPublishInFlight = false;
- this.hashtagPreferencesPublishPromise = null;
-
- if (
- this.hashtagPreferencesService &&
- typeof this.hashtagPreferencesService.on === "function"
- ) {
- this.hashtagPreferencesUnsubscribe = this.hashtagPreferencesService.on(
- "change",
- (detail) => {
- this.handleHashtagPreferencesChange({
- action:
- typeof detail?.action === "string" ? detail.action : "change",
- preferences: detail,
- });
- },
- );
- }
this.dmController.initializeDirectMessagesService();
@@ -446,18 +420,6 @@ export class ProfileModalController {
this.walletDisconnectButton = null;
this.walletStatusText = null;
this.profileWalletStatusText = null;
- this.hashtagStatusText = null;
- this.hashtagBackgroundLoading = false;
- this.hashtagInterestList = null;
- this.hashtagInterestEmpty = null;
- this.hashtagInterestInput = null;
- this.addHashtagInterestButton = null;
- this.profileHashtagInterestRefreshBtn = null;
- this.hashtagDisinterestList = null;
- this.hashtagDisinterestEmpty = null;
- this.hashtagDisinterestInput = null;
- this.addHashtagDisinterestButton = null;
- this.profileHashtagDisinterestRefreshBtn = null;
this.subscriptionsStatusText = null;
this.subscriptionsBackgroundLoading = false;
this.permissionPromptCta = null;
@@ -645,7 +607,7 @@ export class ProfileModalController {
this.authLoadingStateListener,
);
}
- this.populateHashtagPreferences();
+ this.hashtagController.populateHashtagPreferences();
this.refreshModerationSettingsUi();
const preserveMenu = this.isMobileLayoutActive();
this.selectPane(this.getActivePane(), { keepMenuView: preserveMenu });
@@ -854,28 +816,7 @@ export class ProfileModalController {
this.dmController.populateDmRelayPreferences();
this.dmController.syncDmPrivacySettingsUi();
- this.hashtagStatusText =
- document.getElementById("profileHashtagStatus") || null;
- this.hashtagInterestList =
- document.getElementById("profileHashtagInterestList") || null;
- this.hashtagInterestEmpty =
- document.getElementById("profileHashtagInterestEmpty") || null;
- this.hashtagInterestInput =
- document.getElementById("profileHashtagInterestInput") || null;
- this.addHashtagInterestButton =
- document.getElementById("profileAddHashtagInterestBtn") || null;
- this.profileHashtagInterestRefreshBtn =
- document.getElementById("profileHashtagInterestRefreshBtn") || null;
- this.hashtagDisinterestList =
- document.getElementById("profileHashtagDisinterestList") || null;
- this.hashtagDisinterestEmpty =
- document.getElementById("profileHashtagDisinterestEmpty") || null;
- this.hashtagDisinterestInput =
- document.getElementById("profileHashtagDisinterestInput") || null;
- this.addHashtagDisinterestButton =
- document.getElementById("profileAddHashtagDisinterestBtn") || null;
- this.profileHashtagDisinterestRefreshBtn =
- document.getElementById("profileHashtagDisinterestRefreshBtn") || null;
+ this.hashtagController.cacheDomReferences();
this.subscriptionsStatusText =
document.getElementById("subscriptionsStatus") || null;
this.applyPermissionPromptCtaState();
@@ -992,11 +933,11 @@ export class ProfileModalController {
ensureAriaLabel(this.relayController.profileRelayRefreshBtn, "Refresh relay list");
ensureAriaLabel(
- this.profileHashtagInterestRefreshBtn,
+ this.hashtagController.profileHashtagInterestRefreshBtn,
"Refresh interest hashtags",
);
ensureAriaLabel(
- this.profileHashtagDisinterestRefreshBtn,
+ this.hashtagController.profileHashtagDisinterestRefreshBtn,
"Refresh disinterest hashtags",
);
ensureAriaLabel(this.profileSubscriptionsRefreshBtn, "Refresh subscriptions");
@@ -1906,9 +1847,9 @@ export class ProfileModalController {
});
}
- if (this.addHashtagInterestButton instanceof HTMLElement) {
- this.addHashtagInterestButton.addEventListener("click", () => {
- void this.handleAddHashtagPreference("interest");
+ if (this.hashtagController.addHashtagInterestButton instanceof HTMLElement) {
+ this.hashtagController.addHashtagInterestButton.addEventListener("click", () => {
+ void this.hashtagController.handleAddHashtagPreference("interest");
});
}
@@ -1917,7 +1858,7 @@ export class ProfileModalController {
if (!activeHex) {
return;
}
- const service = this.hashtagPreferencesService;
+ const service = this.services.hashtagPreferences;
if (!service || typeof service.load !== "function") {
return;
}
@@ -1931,40 +1872,40 @@ export class ProfileModalController {
});
};
- if (this.profileHashtagInterestRefreshBtn instanceof HTMLElement) {
- this.profileHashtagInterestRefreshBtn.addEventListener(
+ if (this.hashtagController.profileHashtagInterestRefreshBtn instanceof HTMLElement) {
+ this.hashtagController.profileHashtagInterestRefreshBtn.addEventListener(
"click",
handleHashtagRefresh,
);
}
- if (this.profileHashtagDisinterestRefreshBtn instanceof HTMLElement) {
- this.profileHashtagDisinterestRefreshBtn.addEventListener(
+ if (this.hashtagController.profileHashtagDisinterestRefreshBtn instanceof HTMLElement) {
+ this.hashtagController.profileHashtagDisinterestRefreshBtn.addEventListener(
"click",
handleHashtagRefresh,
);
}
- if (this.hashtagInterestInput instanceof HTMLElement) {
- this.hashtagInterestInput.addEventListener("keydown", (event) => {
+ if (this.hashtagController.hashtagInterestInput instanceof HTMLElement) {
+ this.hashtagController.hashtagInterestInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
- void this.handleAddHashtagPreference("interest");
+ void this.hashtagController.handleAddHashtagPreference("interest");
}
});
}
- if (this.addHashtagDisinterestButton instanceof HTMLElement) {
- this.addHashtagDisinterestButton.addEventListener("click", () => {
- void this.handleAddHashtagPreference("disinterest");
+ if (this.hashtagController.addHashtagDisinterestButton instanceof HTMLElement) {
+ this.hashtagController.addHashtagDisinterestButton.addEventListener("click", () => {
+ void this.hashtagController.handleAddHashtagPreference("disinterest");
});
}
- if (this.hashtagDisinterestInput instanceof HTMLElement) {
- this.hashtagDisinterestInput.addEventListener("keydown", (event) => {
+ if (this.hashtagController.hashtagDisinterestInput instanceof HTMLElement) {
+ this.hashtagController.hashtagDisinterestInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
- void this.handleAddHashtagPreference("disinterest");
+ void this.hashtagController.handleAddHashtagPreference("disinterest");
}
});
}
@@ -2312,40 +2253,6 @@ export class ProfileModalController {
return fallback;
}
- describeHashtagPreferencesError(error, options = {}) {
- const describe = this.describeHashtagPreferencesErrorService;
- const { fallbackMessage, operation } =
- options && typeof options === "object" ? options : {};
- const normalizedOperation =
- typeof operation === "string" && operation.trim()
- ? operation.trim().toLowerCase()
- : "update";
-
- const fallback =
- typeof fallbackMessage === "string" && fallbackMessage.trim()
- ? fallbackMessage.trim()
- : normalizedOperation === "load"
- ? "Failed to load hashtag preferences. Please try again."
- : normalizedOperation === "reset"
- ? "Failed to reset hashtag preferences."
- : "Failed to update hashtag preferences. Please try again.";
-
- if (typeof describe === "function") {
- try {
- const message = describe(error, fallback);
- if (typeof message === "string" && message.trim()) {
- return message.trim();
- }
- } catch (describeError) {
- devLogger.warn(
- "[ProfileModalController] describeHashtagPreferencesError service threw:",
- describeError,
- );
- }
- }
-
- return fallback;
- }
setAddAccountLoading(isLoading) {
if (!(this.addAccountButton instanceof HTMLElement)) {
@@ -3643,13 +3550,13 @@ export class ProfileModalController {
} else if (target === "storage") {
this.storageController.populateStoragePane();
} else if (target === "hashtags") {
- this.populateHashtagPreferences();
- if (activeHex && this.hashtagPreferencesService) {
- this.hashtagPreferencesService
+ this.hashtagController.populateHashtagPreferences();
+ if (activeHex && this.services.hashtagPreferences) {
+ this.services.hashtagPreferences
.load(activeHex, { allowPermissionPrompt: true })
.catch(noop);
}
- this.refreshHashtagBackgroundStatus();
+ this.hashtagController.refreshHashtagBackgroundStatus();
} else if (target === "subscriptions") {
if (activeHex && this.subscriptionsService) {
this.subscriptionsService
@@ -3777,91 +3684,6 @@ export class ProfileModalController {
}
}
- normalizeHashtagTag(value) {
- return normalizeHashtag(value);
- }
-
- formatHashtagTag(value) {
- return formatHashtag(value);
- }
-
- sanitizeHashtagList(list) {
- if (!Array.isArray(list)) {
- return [];
- }
-
- const seen = new Set();
- const normalized = [];
-
- list.forEach((entry) => {
- if (typeof entry !== "string") {
- return;
- }
- const tag = this.normalizeHashtagTag(entry);
- if (!tag || seen.has(tag)) {
- return;
- }
- seen.add(tag);
- normalized.push(tag);
- });
-
- normalized.sort((a, b) => a.localeCompare(b));
- return normalized;
- }
-
- getResolvedHashtagPreferences(preferences = null) {
- const candidate =
- preferences && typeof preferences === "object" ? preferences : null;
-
- let snapshot = null;
- if (candidate) {
- snapshot = candidate;
- } else if (typeof this.getHashtagPreferencesSnapshotService === "function") {
- try {
- const resolved = this.getHashtagPreferencesSnapshotService();
- if (resolved && typeof resolved === "object") {
- snapshot = resolved;
- }
- } catch (error) {
- devLogger.warn(
- "[ProfileModalController] Failed to read hashtag preferences snapshot:",
- error,
- );
- }
- }
-
- const service = this.hashtagPreferencesService || {};
- const loadState =
- typeof service.getLoadState === "function" ? service.getLoadState() : null;
-
- const interestsSource = Array.isArray(snapshot?.interests)
- ? snapshot.interests
- : typeof service.getInterests === "function"
- ? service.getInterests()
- : [];
- const disinterestsSource = Array.isArray(snapshot?.disinterests)
- ? snapshot.disinterests
- : typeof service.getDisinterests === "function"
- ? service.getDisinterests()
- : [];
-
- return {
- interests: this.sanitizeHashtagList(interestsSource),
- disinterests: this.sanitizeHashtagList(disinterestsSource),
- uiReady:
- loadState?.uiReady === true ||
- service.uiReady === true ||
- (!loadState && service.uiReady !== false),
- dataReady:
- loadState?.dataReady === true ||
- service.dataReady === true ||
- (!loadState && service.dataReady !== false),
- lastLoadError: loadState?.lastLoadError || service.lastLoadError || null,
- loadedFromCache:
- loadState?.loadedFromCache === true || service.loadedFromCache === true,
- };
- }
-
setPermissionPromptCtaState(nextState = {}) {
if (!nextState || typeof nextState !== "object") {
return;
@@ -3938,7 +3760,7 @@ export class ProfileModalController {
"Subscriptions loaded from cache; sync is retrying in background.",
"warning",
);
- this.setHashtagStatus(
+ this.hashtagController.setHashtagStatus(
"Hashtag preferences loaded from cache; sync is retrying in background.",
"warning",
);
@@ -3950,7 +3772,7 @@ export class ProfileModalController {
"Some profile lists failed to sync. Use Retry sync.",
"warning",
);
- this.setHashtagStatus(
+ this.hashtagController.setHashtagStatus(
"Some profile lists failed to sync. Use Retry sync.",
"warning",
);
@@ -3983,49 +3805,6 @@ export class ProfileModalController {
}
}
- setHashtagStatus(message = "", tone = "muted") {
- if (!(this.hashtagStatusText instanceof HTMLElement)) {
- return;
- }
-
- const classList = this.hashtagStatusText.classList;
- classList.remove(
- "text-status-success",
- "text-status-warning",
- "text-status-danger",
- "text-status-info",
- "text-muted",
- );
-
- const normalized =
- typeof message === "string" && message.trim() ? message.trim() : "";
-
- if (!normalized) {
- this.hashtagStatusText.textContent = "";
- this.hashtagStatusText.classList.add("text-muted", "hidden");
- return;
- }
-
- this.hashtagStatusText.textContent = normalized;
- this.hashtagStatusText.classList.remove("hidden");
-
- switch (tone) {
- case "success":
- classList.add("text-status-success");
- break;
- case "warning":
- case "error":
- classList.add("text-status-warning");
- break;
- case "info":
- classList.add("text-status-info");
- break;
- default:
- classList.add("text-muted");
- break;
- }
- }
-
setSubscriptionsStatus(message = "", tone = "muted") {
if (!(this.subscriptionsStatusText instanceof HTMLElement)) {
return;
@@ -4069,26 +3848,6 @@ export class ProfileModalController {
}
}
- refreshHashtagBackgroundStatus() {
- const isBackground = this.hashtagPreferencesService?.backgroundLoading === true;
- const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
-
- if (isBackground && !this.hashtagBackgroundLoading) {
- this.hashtagBackgroundLoading = true;
- if (!statusText) {
- this.setHashtagStatus("Loading in background…", "info");
- }
- return;
- }
-
- if (!isBackground && this.hashtagBackgroundLoading) {
- if (statusText === "Loading in background…") {
- this.setHashtagStatus("", "muted");
- }
- this.hashtagBackgroundLoading = false;
- }
- }
-
refreshSubscriptionsBackgroundStatus() {
const isBackground = this.subscriptionsService?.backgroundLoading === true;
const statusText = this.subscriptionsStatusText?.textContent?.trim?.() || "";
@@ -4109,391 +3868,6 @@ export class ProfileModalController {
}
}
- clearHashtagInputs() {
- if (this.hashtagInterestInput instanceof HTMLInputElement) {
- this.hashtagInterestInput.value = "";
- }
- if (this.hashtagDisinterestInput instanceof HTMLInputElement) {
- this.hashtagDisinterestInput.value = "";
- }
- }
-
- populateHashtagPreferences(preferences = null) {
- const snapshot = this.getResolvedHashtagPreferences(preferences);
-
- if (!snapshot.uiReady) {
- this.setHashtagStatus("Loading hashtag preferences…", "info");
- this.renderHashtagList("interest", []);
- this.renderHashtagList("disinterest", []);
- this.refreshHashtagBackgroundStatus();
- return;
- }
-
- if (!snapshot.dataReady) {
- const message = snapshot.lastLoadError
- ? "Couldn’t sync hashtag preferences. Retry to load your lists."
- : "Hashtag preferences are unavailable right now. Retry to sync.";
- this.setHashtagStatus(message, "warning");
- this.renderHashtagList("interest", []);
- this.renderHashtagList("disinterest", []);
- this.refreshHashtagBackgroundStatus();
- return;
- }
-
- this.renderHashtagList("interest", snapshot.interests);
- this.renderHashtagList("disinterest", snapshot.disinterests);
-
- if (!snapshot.interests.length && !snapshot.disinterests.length) {
- if (snapshot.loadedFromCache) {
- this.setHashtagStatus(
- "No hashtag preferences yet (showing cached state).",
- "info",
- );
- } else {
- this.setHashtagStatus("", "muted");
- }
- }
- this.refreshHashtagBackgroundStatus();
- }
-
- renderHashtagList(type, tags) {
- const list =
- type === "interest" ? this.hashtagInterestList : this.hashtagDisinterestList;
- const empty =
- type === "interest" ? this.hashtagInterestEmpty : this.hashtagDisinterestEmpty;
-
- if (!(list instanceof HTMLElement) || !(empty instanceof HTMLElement)) {
- return;
- }
-
- list.textContent = "";
-
- const normalized = this.sanitizeHashtagList(tags);
- if (!normalized.length) {
- empty.classList.remove("hidden");
- list.classList.add("hidden");
- return;
- }
-
- empty.classList.add("hidden");
- list.classList.remove("hidden");
-
- normalized.forEach((tag) => {
- const item = this.createHashtagListItem(type, tag);
- if (item) {
- list.appendChild(item);
- }
- });
- }
-
- createHashtagListItem(type, tag) {
- const normalized = this.normalizeHashtagTag(tag);
- if (!normalized) {
- return null;
- }
-
- const item = document.createElement("li");
- item.className = "profile-hashtag-item";
- item.dataset.hashtagType = type;
- item.dataset.tag = normalized;
-
- const label = document.createElement("span");
- label.textContent = this.formatHashtagTag(normalized);
- item.appendChild(label);
-
- const removeButton = document.createElement("button");
- removeButton.type = "button";
- removeButton.className = "profile-hashtag-remove focus-ring";
- removeButton.dataset.hashtagType = type;
- removeButton.dataset.tag = normalized;
- removeButton.setAttribute(
- "aria-label",
- type === "interest"
- ? `Remove ${this.formatHashtagTag(normalized)} from interests`
- : `Remove ${this.formatHashtagTag(normalized)} from disinterests`,
- );
- const icon = document.createElement("span");
- icon.setAttribute("aria-hidden", "true");
- icon.textContent = "\u00D7";
- removeButton.appendChild(icon);
- removeButton.addEventListener("click", () => {
- void this.handleRemoveHashtagPreference(type, normalized);
- });
-
- item.appendChild(removeButton);
-
- return item;
- }
-
- async persistHashtagPreferences(options = {}) {
- const service = this.hashtagPreferencesService;
- const publish =
- service && typeof service.publish === "function" ? service.publish : null;
-
- if (!publish) {
- const message = this.describeHashtagPreferencesError(null, {
- fallbackMessage: "Hashtag preferences are unavailable right now.",
- });
- if (message) {
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- }
- const error = new Error(
- message || "Hashtag preferences are unavailable right now.",
- );
- error.code = "service-unavailable";
- throw error;
- }
-
- if (this.hashtagPreferencesPublishInFlight) {
- return this.hashtagPreferencesPublishPromise;
- }
-
- const { successMessage, pubkey, progressMessage } =
- options && typeof options === "object" ? options : {};
-
- const resolvedPubkeyCandidate =
- typeof pubkey === "string" && pubkey.trim()
- ? pubkey
- : this.getActivePubkey();
- const normalizedPubkey = this.normalizeHexPubkey(resolvedPubkeyCandidate);
-
- const payload = normalizedPubkey ? { pubkey: normalizedPubkey } : {};
-
- const pendingMessage =
- typeof progressMessage === "string" && progressMessage.trim()
- ? progressMessage.trim()
- : "Saving hashtag preferences…";
- const finalMessage =
- typeof successMessage === "string" && successMessage.trim()
- ? successMessage.trim()
- : "Hashtag preferences saved.";
-
- this.hashtagPreferencesPublishInFlight = true;
- this.setHashtagStatus(pendingMessage, "info");
-
- const publishPromise = (async () => {
- try {
- const result = await publish.call(service, payload);
- this.setHashtagStatus(finalMessage, "success");
- return result;
- } catch (error) {
- const failure =
- error instanceof Error ? error : new Error(String(error || ""));
- if (!failure.code) {
- failure.code = "hashtag-preferences-publish-failed";
- }
- const message = this.describeHashtagPreferencesError(failure, {
- fallbackMessage:
- "Failed to update hashtag preferences. Please try again.",
- });
- if (message) {
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- }
- throw failure;
- } finally {
- this.hashtagPreferencesPublishInFlight = false;
- this.hashtagPreferencesPublishPromise = null;
- }
- })();
-
- this.hashtagPreferencesPublishPromise = publishPromise;
- return publishPromise;
- }
-
- async handleAddHashtagPreference(type) {
- const isInterest = type === "interest";
- const input = isInterest
- ? this.hashtagInterestInput
- : this.hashtagDisinterestInput;
-
- const rawValue =
- input instanceof HTMLInputElement ? input.value || "" : "";
- const normalized = this.normalizeHashtagTag(rawValue);
-
- if (!(input instanceof HTMLInputElement)) {
- return { success: false, reason: "missing-input" };
- }
-
- if (!normalized) {
- const message = "Enter a hashtag to add.";
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- return { success: false, reason: "empty" };
- }
-
- const service = this.hashtagPreferencesService;
- const addMethod = isInterest
- ? service?.addInterest
- : service?.addDisinterest;
-
- if (typeof addMethod !== "function") {
- const message = "Hashtag preferences are unavailable right now.";
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- return { success: false, reason: "service-unavailable" };
- }
-
- const snapshot = this.getResolvedHashtagPreferences();
- const alreadyInTarget = isInterest
- ? snapshot.interests.includes(normalized)
- : snapshot.disinterests.includes(normalized);
- const inOpposite = isInterest
- ? snapshot.disinterests.includes(normalized)
- : snapshot.interests.includes(normalized);
-
- let result = false;
- try {
- result = addMethod.call(service, normalized);
- } catch (error) {
- const message = this.describeHashtagPreferencesError(error, {
- fallbackMessage: "Failed to update hashtag preferences. Please try again.",
- });
- if (message) {
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- }
- return { success: false, reason: error?.code || "service-error", error };
- } finally {
- if (input) {
- input.value = "";
- }
- }
-
- if (result) {
- const actionMessage = inOpposite
- ? `${this.formatHashtagTag(normalized)} moved to ${
- isInterest ? "interests" : "disinterests"
- }.`
- : `${this.formatHashtagTag(normalized)} added to ${
- isInterest ? "interests" : "disinterests"
- }.`;
- this.populateHashtagPreferences();
- try {
- await this.persistHashtagPreferences({
- successMessage: actionMessage,
- });
- this.showSuccess(actionMessage);
- return { success: true, reason: inOpposite ? "moved" : "added" };
- } catch (error) {
- return {
- success: false,
- reason: error?.code || "publish-failed",
- error,
- };
- } finally {
- this.populateHashtagPreferences();
- }
- }
-
- if (alreadyInTarget) {
- const message = `${this.formatHashtagTag(normalized)} is already in your ${
- isInterest ? "interests" : "disinterests"
- }.`;
- this.showStatus(message);
- this.setHashtagStatus(message, "info");
- this.populateHashtagPreferences();
- return { success: false, reason: "duplicate" };
- }
-
- const fallbackMessage = this.describeHashtagPreferencesError(null, {
- fallbackMessage: `Failed to add ${this.formatHashtagTag(normalized)}.`,
- });
- if (fallbackMessage) {
- this.showError(fallbackMessage);
- this.setHashtagStatus(fallbackMessage, "warning");
- }
- this.populateHashtagPreferences();
- return { success: false, reason: "no-change" };
- }
-
- async handleRemoveHashtagPreference(type, candidate) {
- const normalized = this.normalizeHashtagTag(candidate);
- if (!normalized) {
- return { success: false, reason: "invalid" };
- }
-
- const service = this.hashtagPreferencesService;
- const removeMethod =
- type === "interest"
- ? service?.removeInterest
- : service?.removeDisinterest;
-
- if (typeof removeMethod !== "function") {
- const message = "Hashtag preferences are unavailable right now.";
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- return { success: false, reason: "service-unavailable" };
- }
-
- let removed = false;
- try {
- removed = removeMethod.call(service, normalized);
- } catch (error) {
- const message = this.describeHashtagPreferencesError(error, {
- fallbackMessage: `Failed to remove ${this.formatHashtagTag(normalized)}.`,
- });
- if (message) {
- this.showError(message);
- this.setHashtagStatus(message, "warning");
- }
- this.populateHashtagPreferences();
- return { success: false, reason: error?.code || "service-error", error };
- }
-
- if (removed) {
- const message = `${this.formatHashtagTag(normalized)} removed from ${
- type === "interest" ? "interests" : "disinterests"
- }.`;
- this.populateHashtagPreferences();
- try {
- await this.persistHashtagPreferences({ successMessage: message });
- this.showSuccess(message);
- } catch (error) {
- return {
- success: false,
- reason: error?.code || "publish-failed",
- error,
- };
- } finally {
- this.populateHashtagPreferences();
- }
- } else {
- const message = `${this.formatHashtagTag(normalized)} is already removed.`;
- this.showStatus(message);
- this.setHashtagStatus(message, "info");
- }
-
- this.populateHashtagPreferences();
- return { success: removed, reason: removed ? "removed" : "already-removed" };
- }
-
- handleHashtagPreferencesChange(detail = {}) {
- const preferences =
- detail && typeof detail.preferences === "object"
- ? detail.preferences
- : detail;
- const action = typeof detail?.action === "string" ? detail.action : "";
-
- if (action === "background-loading") {
- this.hashtagBackgroundLoading = true;
- this.setHashtagStatus("Loading in background…", "info");
- } else if (
- (action === "sync" || action === "background-loaded" || action === "reset") &&
- this.hashtagBackgroundLoading
- ) {
- const statusText = this.hashtagStatusText?.textContent?.trim?.() || "";
- if (statusText === "Loading in background…") {
- this.setHashtagStatus("", "muted");
- }
- this.hashtagBackgroundLoading = false;
- }
-
- this.populateHashtagPreferences(preferences);
- this.refreshHashtagBackgroundStatus();
- }
-
handleSubscriptionsChange(detail = {}) {
const action = typeof detail?.action === "string" ? detail.action : "";
@@ -8424,7 +7798,7 @@ export class ProfileModalController {
void this.populateFriendsList();
this.relayController.populateProfileRelays();
this.walletController.refreshWalletPaneState();
- this.populateHashtagPreferences();
+ this.hashtagController.populateHashtagPreferences();
void this.storageController.populateStoragePane();
this.handleActiveDmIdentityChanged(activePubkey);
void this.refreshDmRelayPreferences({ force: true });
@@ -8438,7 +7812,7 @@ export class ProfileModalController {
void this.populateFriendsList();
this.relayController.populateProfileRelays();
this.walletController.refreshWalletPaneState();
- this.populateHashtagPreferences();
+ this.hashtagController.populateHashtagPreferences();
void this.storageController.populateStoragePane();
void this.refreshDmRelayPreferences({ force: true });
})
@@ -8504,10 +7878,10 @@ export class ProfileModalController {
this.clearFriendsList();
this.relayController.populateProfileRelays();
this.walletController.refreshWalletPaneState();
- this.populateHashtagPreferences();
+ this.hashtagController.populateHashtagPreferences();
void this.storageController.populateStoragePane();
- this.clearHashtagInputs();
- this.setHashtagStatus("", "muted");
+ this.hashtagController.clearHashtagInputs();
+ this.hashtagController.setHashtagStatus("", "muted");
this.handleActiveDmIdentityChanged(null);
this.setMessagesUnreadIndicator(false);
this.dmController.populateDmRelayPreferences();
@@ -8751,6 +8125,34 @@ export class ProfileModalController {
get profileRelayRefreshBtn() {
return this.relayController.profileRelayRefreshBtn;
}
+
+ // Hashtag Controller Delegates
+ get hashtagStatusText() { return this.hashtagController.hashtagStatusText; }
+ get hashtagBackgroundLoading() { return this.hashtagController.hashtagBackgroundLoading; }
+ set hashtagBackgroundLoading(val) { this.hashtagController.hashtagBackgroundLoading = val; }
+ get hashtagInterestList() { return this.hashtagController.hashtagInterestList; }
+ get hashtagInterestEmpty() { return this.hashtagController.hashtagInterestEmpty; }
+ get hashtagInterestInput() { return this.hashtagController.hashtagInterestInput; }
+ get addHashtagInterestButton() { return this.hashtagController.addHashtagInterestButton; }
+ get profileHashtagInterestRefreshBtn() { return this.hashtagController.profileHashtagInterestRefreshBtn; }
+ get hashtagDisinterestList() { return this.hashtagController.hashtagDisinterestList; }
+ get hashtagDisinterestEmpty() { return this.hashtagController.hashtagDisinterestEmpty; }
+ get hashtagDisinterestInput() { return this.hashtagController.hashtagDisinterestInput; }
+ get addHashtagDisinterestButton() { return this.hashtagController.addHashtagDisinterestButton; }
+ get profileHashtagDisinterestRefreshBtn() { return this.hashtagController.profileHashtagDisinterestRefreshBtn; }
+
+ populateHashtagPreferences(...args) { return this.hashtagController.populateHashtagPreferences(...args); }
+ handleAddHashtagPreference(...args) { return this.hashtagController.handleAddHashtagPreference(...args); }
+ handleRemoveHashtagPreference(...args) { return this.hashtagController.handleRemoveHashtagPreference(...args); }
+ handleHashtagPreferencesChange(...args) { return this.hashtagController.handleHashtagPreferencesChange(...args); }
+ normalizeHashtagTag(...args) { return this.hashtagController.normalizeHashtagTag(...args); }
+ formatHashtagTag(...args) { return this.hashtagController.formatHashtagTag(...args); }
+ sanitizeHashtagList(...args) { return this.hashtagController.sanitizeHashtagList(...args); }
+ getResolvedHashtagPreferences(...args) { return this.hashtagController.getResolvedHashtagPreferences(...args); }
+ setHashtagStatus(...args) { return this.hashtagController.setHashtagStatus(...args); }
+ refreshHashtagBackgroundStatus(...args) { return this.hashtagController.refreshHashtagBackgroundStatus(...args); }
+ clearHashtagInputs(...args) { return this.hashtagController.clearHashtagInputs(...args); }
+ describeHashtagPreferencesError(...args) { return this.hashtagController.describeHashtagPreferencesError(...args); }
}
export default ProfileModalController;
diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs
index e4b9bb34..525940d7 100644
--- a/scripts/check-file-size.mjs
+++ b/scripts/check-file-size.mjs
@@ -26,7 +26,7 @@ const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much f
// These files are known to be oversized and are decomposition targets.
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
const GRANDFATHERED = {
- "js/ui/profileModalController.js": 8753,
+ "js/ui/profileModalController.js": 8130,
"js/ui/components/VideoModal.js": 6284,
"js/app.js": 5602,
"js/channelProfile.js": 5529,