···11+# Twisted — Tangled Mobile Companion
22+33+A mobile-first Tangled client for iOS, Android, and web. Built with Ionic Vue, Capacitor, and the `@atcute` AT Protocol client stack.
44+55+## What is Tangled
66+77+[Tangled](https://tangled.org) is a Git hosting and collaboration platform built on the [AT Protocol](https://atproto.com). Identity, social graph (follows, stars, reactions), repos, issues, and PRs are all AT Protocol records stored on users' Personal Data Servers. Git hosting runs on **knots** — headless servers exposing XRPC APIs. The **appview** at `tangled.org` aggregates and renders the network view.
88+99+- Docs: <https://docs.tangled.org>
1010+- Lexicon namespace: `sh.tangled.*`
1111+- Source: <https://tangled.org/tangled.org/core>
1212+1313+## What Twisted Does
1414+1515+**Reader and social companion** for Tangled. Focused on discovery, browsing, and lightweight interactions.
1616+1717+- Browse repos, files, READMEs, issues, PRs
1818+- Discover trending/recent repos and users
1919+- Activity feed (global and personalized)
2020+- Sign in via AT Protocol OAuth
2121+- Star repos, follow users, react to content
2222+- Offline-capable with cached data
2323+2424+Out of scope: repo creation, git push/pull, CI/CD, full code review authoring.
2525+2626+## Technology
2727+2828+| Layer | Choice |
2929+| ----------- | --------------------------------------------------------------------------------------------- |
3030+| Framework | Vue 3 + TypeScript |
3131+| UI | Ionic Vue |
3232+| Native | Capacitor (iOS, Android, Web) |
3333+| State | Pinia |
3434+| Async data | TanStack Query (Vue) |
3535+| AT Protocol | `@atcute/client` (XRPC), `@atcute/oauth-browser-client` (OAuth), `@atcute/tangled` (lexicons) |
3636+3737+## Architecture
3838+3939+Three layers, strict dependency direction (presentation → domain → data):
4040+4141+**Presentation** — Ionic pages, Vue components, composables, Pinia stores.
4242+**Domain** — Normalized models (`UserSummary`, `RepoDetail`, `ActivityItem`, etc.), action policies, pagination.
4343+**Data** — `@atcute/client` XRPC calls, `@atcute/tangled` type definitions, local cache, optional BFF.
4444+4545+Protocol isolation: no Vue component imports `@atcute/*` directly. All API access flows through `src/services/`.
4646+4747+## Tangled API Surface
4848+4949+Two distinct API hosts:
5050+5151+| Host | Protocol | Data |
5252+| ---------------------------------- | ---------------------------------- | ----------------------------------------------------------------- |
5353+| Knots (`us-west.tangled.sh`, etc.) | XRPC at `/xrpc/sh.tangled.*` | Git data: trees, blobs, commits, branches, diffs, tags |
5454+| User's PDS | XRPC at `/xrpc/com.atproto.repo.*` | AT Protocol records: repos, issues, PRs, stars, follows, profiles |
5555+5656+The appview (`tangled.org`) serves HTML — it's the web UI, not a JSON API. The mobile client talks to knots and PDS servers directly.
5757+5858+Repo param format: `did:plc:xxx/repoName`.
5959+6060+## Phases
6161+6262+| Phase | Focus | Spec | Tasks |
6363+| ----- | ------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------ |
6464+| 1 | Project shell, tabs, mock data, design system | [specs/phase-1.md](specs/phase-1.md) | [tasks/phase-1.md](tasks/phase-1.md) |
6565+| 2 | Public browsing — repos, files, profiles, issues, PRs | [specs/phase-2.md](specs/phase-2.md) | [tasks/phase-2.md](tasks/phase-2.md) |
6666+| 3 | Search, discovery, activity feed | [specs/phase-3.md](specs/phase-3.md) | [tasks/phase-3.md](tasks/phase-3.md) |
6767+| 4 | OAuth sign-in, star, follow, react, personalized feed | [specs/phase-4.md](specs/phase-4.md) | [tasks/phase-4.md](tasks/phase-4.md) |
6868+| 5 | Offline persistence, performance, bundle optimization | [specs/phase-5.md](specs/phase-5.md) | [tasks/phase-5.md](tasks/phase-5.md) |
6969+| 6 | Write features (issues, comments, profile edit), BFF, push notifications | [specs/phase-6.md](specs/phase-6.md) | [tasks/phase-6.md](tasks/phase-6.md) |
7070+| 7 | Real-time Jetstream feed, custom feeds, forking, labels, interdiff | [specs/phase-7.md](specs/phase-7.md) | [tasks/phase-7.md](tasks/phase-7.md) |
7171+7272+## Key Design Decisions
7373+7474+1. **`@atcute` end-to-end** for all AT Protocol interaction — no mixing client stacks.
7575+2. **Tangled lexicon handling in one module boundary** (`src/services/tangled/`) — don't scatter `sh.tangled.*` awareness across pages.
7676+3. **Read-first** — the primary product is a fast reader. Social mutations are a controlled second layer.
7777+4. **Thin BFF when needed** (Phase 6+) for search indexing, personalized feeds, push notifications, and unstable procedure wrapping.
7878+5. **Mobile-first, not desktop-forge-first** — prioritize discovery, readability, feed-driven interactions, small focused actions.
+180
docs/specs/phase-1.md
···11+# Phase 1 — Project Shell & Design System
22+33+## Goal
44+55+Scaffold the Ionic Vue project with tab navigation, placeholder pages, mock data, and reusable UI primitives. Nothing touches the network. The result is a clickable prototype that validates navigation, layout, and component design before any API integration.
66+77+## Technology Stack
88+99+| Layer | Choice |
1010+| -------------- | ----------------------- |
1111+| Framework | Vue 3 + TypeScript |
1212+| UI kit | Ionic Vue |
1313+| Native runtime | Capacitor |
1414+| State | Pinia |
1515+| Async data | TanStack Query (Vue) |
1616+| Routing | Vue Router (Ionic tabs) |
1717+1818+## Navigation Structure
1919+2020+Five-tab layout:
2121+2222+1. **Home** — trending repos, recent activity, personalized content (auth)
2323+2. **Explore** — search repos/users, filters
2424+3. **Repo** — deep-link target for repository detail (not a persistent tab icon — navigated to from Home/Explore/Activity)
2525+4. **Activity** — global feed (anon), social graph feed (auth)
2626+5. **Profile** — auth state, user card, follows, starred repos, settings
2727+2828+> Repo is a routed detail destination, not a standing tab. The tab bar shows Home, Explore, Activity, Profile. Repo pages are pushed onto the Home/Explore/Activity stacks.
2929+3030+## Directory Layout
3131+3232+```sh
3333+src/
3434+ app/
3535+ router/ # route definitions, tab guards
3636+ boot/ # app-level setup (query client, plugins)
3737+ providers/ # provide/inject wrappers
3838+ core/
3939+ config/ # env, feature flags
4040+ errors/ # error types and normalization
4141+ storage/ # storage abstraction (IndexedDB / Capacitor Secure Storage)
4242+ query/ # TanStack Query client config, persister setup
4343+ auth/ # auth state machine, session store
4444+ services/
4545+ atproto/ # @atcute/client wrapper, identity helpers
4646+ tangled/ # Tangled API: endpoints, adapters, normalizers, queries, mutations
4747+ domain/
4848+ models/ # UserSummary, RepoSummary, RepoDetail, etc.
4949+ feed/ # feed-specific types and helpers
5050+ repo/ # repo-specific types and helpers
5151+ profile/ # profile-specific types and helpers
5252+ features/
5353+ home/
5454+ explore/
5555+ repo/
5656+ activity/
5757+ profile/
5858+ components/
5959+ common/ # cards, buttons, loaders, empty states, error boundaries
6060+ repo/ # repo card, file tree item, README viewer
6161+ feed/ # activity card, feed list
6262+ profile/ # user card, follow button
6363+```
6464+6565+## Domain Models
6666+6767+```ts
6868+export type UserSummary = {
6969+ did: string;
7070+ handle: string;
7171+ displayName?: string;
7272+ avatar?: string;
7373+ bio?: string;
7474+ followerCount?: number;
7575+ followingCount?: number;
7676+};
7777+7878+export type RepoSummary = {
7979+ atUri: string;
8080+ ownerDid: string;
8181+ ownerHandle: string;
8282+ name: string;
8383+ description?: string;
8484+ primaryLanguage?: string;
8585+ stars?: number;
8686+ forks?: number;
8787+ updatedAt?: string;
8888+ knot: string;
8989+};
9090+9191+export type RepoDetail = RepoSummary & {
9292+ readme?: string;
9393+ defaultBranch?: string;
9494+ languages?: Record<string, number>;
9595+ collaborators?: UserSummary[];
9696+ topics?: string[];
9797+};
9898+9999+export type RepoFile = {
100100+ path: string;
101101+ name: string;
102102+ type: "file" | "dir" | "submodule";
103103+ size?: number;
104104+ lastCommitMessage?: string;
105105+};
106106+107107+export type PullRequestSummary = {
108108+ atUri: string;
109109+ title: string;
110110+ authorDid: string;
111111+ authorHandle: string;
112112+ status: "open" | "merged" | "closed";
113113+ createdAt: string;
114114+ updatedAt?: string;
115115+ sourceBranch: string;
116116+ targetBranch: string;
117117+ roundCount?: number;
118118+};
119119+120120+export type IssueSummary = {
121121+ atUri: string;
122122+ title: string;
123123+ authorDid: string;
124124+ authorHandle: string;
125125+ state: "open" | "closed";
126126+ createdAt: string;
127127+ commentCount?: number;
128128+};
129129+130130+export type ActivityItem = {
131131+ id: string;
132132+ kind:
133133+ | "repo_created"
134134+ | "repo_starred"
135135+ | "user_followed"
136136+ | "pr_opened"
137137+ | "pr_merged"
138138+ | "issue_opened"
139139+ | "issue_closed";
140140+ actorDid: string;
141141+ actorHandle: string;
142142+ targetUri?: string;
143143+ targetName?: string;
144144+ createdAt: string;
145145+};
146146+```
147147+148148+## Repo Detail Page Structure
149149+150150+Segmented tab layout within the repo detail view:
151151+152152+| Segment | Content |
153153+| -------- | ------------------------------------------------------------------------------------ |
154154+| Overview | owner/repo header, description, topics, social action buttons, README preview, stats |
155155+| Files | directory tree, file viewer (syntax-highlighted) |
156156+| Issues | issue list with state filters |
157157+| PRs | pull request list with status filters |
158158+159159+## Design System Primitives
160160+161161+Build these reusable components during this phase:
162162+163163+- **RepoCard** — compact repo summary for lists
164164+- **UserCard** — avatar + handle + bio snippet
165165+- **ActivityCard** — icon + actor + verb + target + timestamp
166166+- **FileTreeItem** — icon (file/dir) + name + last commit message
167167+- **EmptyState** — icon + message + optional action button
168168+- **ErrorBoundary** — catch + retry UI
169169+- **SkeletonLoader** — content placeholder shimmer for each card type
170170+- **MarkdownRenderer** — render README content (Phase 2 will wire to real data)
171171+172172+## Mock Data
173173+174174+Create `src/mocks/` with factory functions returning typed domain models. All Phase 1 screens render from these factories. Mock data must be realistic — use real-looking handles (`alice.tngl.sh`), repo names, and timestamps.
175175+176176+## Performance Targets
177177+178178+- Shell first-paint under 2s on mid-range device
179179+- Tab switches feel instant (no layout shift)
180180+- Skeleton loaders shown within 100ms of navigation
+115
docs/specs/phase-2.md
···11+# Phase 2 — Public Tangled Browsing
22+33+## Goal
44+55+Replace mock data with live Tangled API calls. Users can browse repos, profiles, file trees, README content, issues, and pull requests without signing in.
66+77+## Protocol Stack
88+99+| Package | Version | Role |
1010+| ----------------- | ------- | ---------------------------------------------- |
1111+| `@atcute/client` | ^4.2.1 | XRPC HTTP client — `query()` and `procedure()` |
1212+| `@atcute/tangled` | ^1.0.17 | `sh.tangled.*` lexicon type definitions |
1313+1414+All protocol access goes through `src/services/tangled/`. No Vue component may import `@atcute/*` directly.
1515+1616+## Architecture: Protocol Isolation
1717+1818+```sh
1919+Vue component
2020+ → composable (useRepoDetail, useFileTree, ...)
2121+ → TanStack Query hook
2222+ → service function (services/tangled/queries.ts)
2323+ → @atcute/client XRPC call
2424+ → normalizer (services/tangled/normalizers.ts)
2525+ → domain model
2626+```
2727+2828+### Service Layer Responsibilities
2929+3030+**`services/atproto/client.ts`** — singleton `XRPC` client instance, base URL config, error interceptor.
3131+3232+**`services/tangled/endpoints.ts`** — typed wrappers around XRPC queries:
3333+3434+| Endpoint | Params | Returns |
3535+| ---------------------------------- | ------------------------------------------- | ------------------- |
3636+| `sh.tangled.repo.tree` | `repo: did:plc:xxx/name`, `ref`, `path?` | directory listing |
3737+| `sh.tangled.repo.blob` | `repo`, `ref`, `path` | file content |
3838+| `sh.tangled.repo.log` | `repo`, `ref`, `path?`, `limit?`, `cursor?` | commit history |
3939+| `sh.tangled.repo.branches` | `repo`, `limit?`, `cursor?` | branch list |
4040+| `sh.tangled.repo.tags` | `repo` | tag list |
4141+| `sh.tangled.repo.getDefaultBranch` | `repo` | default branch name |
4242+| `sh.tangled.repo.diff` | `repo`, `ref` | diff output |
4343+| `sh.tangled.repo.compare` | `repo`, `rev1`, `rev2` | comparison |
4444+| `sh.tangled.repo.languages` | `repo` | language breakdown |
4545+4646+The `repo` param format is `did:plc:xxx/repoName`. The XRPC calls go to the repo's **knot** hostname (e.g., `us-west.tangled.sh`), not to `tangled.org`.
4747+4848+**`services/tangled/normalizers.ts`** — transform raw lexicon responses into domain models (`RepoSummary`, `RepoDetail`, `RepoFile`, etc.).
4949+5050+**`services/tangled/queries.ts`** — TanStack Query wrapper functions with cache keys, stale times, and error handling.
5151+5252+## Appview vs Knot Routing
5353+5454+Tangled has two API surfaces:
5555+5656+| Surface | Host | Protocol | Used for |
5757+| ------- | -------------------------- | --------------------------- | ------------------------------------------------- |
5858+| Appview | `tangled.org` | HTTP (HTML, HTMX) | Profile pages, repo listings, timeline, search |
5959+| Knots | `us-west.tangled.sh`, etc. | XRPC (`/xrpc/sh.tangled.*`) | Git data — trees, blobs, commits, branches, diffs |
6060+6161+For Phase 2, git data comes from knots via XRPC. Profile and repo metadata comes from the appview. The service layer must route requests to the correct host based on the operation.
6262+6363+> **Open question**: The appview serves HTML, not JSON API responses. We may need to scrape, use AT Protocol PDS queries (`com.atproto.repo.getRecord`), or discover if the appview exposes a JSON API. This must be validated early in Phase 2.
6464+6565+## Features
6666+6767+### Repository Browsing
6868+6969+- List repos for a user (from their PDS records or appview)
7070+- Repo overview: metadata, description, topics, default branch, language stats
7171+- README rendering: fetch blob for `README.md` from default branch, render markdown
7272+- File tree: navigate directories, open files
7373+- File viewer: syntax-highlighted source display
7474+- Commit log: paginated history for a ref/path
7575+- Branch list with default branch indicator
7676+7777+### Profile Browsing
7878+7979+- View user profile: avatar, bio, links, pronouns, location, pinned repos
8080+- Profile data comes from `sh.tangled.actor.profile` record (key: `self`) on the user's PDS
8181+- List user's repos
8282+8383+### Pull Requests (read-only)
8484+8585+- List PRs for a repo with status filter (open/closed/merged)
8686+- PR detail: title, body, author, source/target branches, round count
8787+- PR comments list
8888+8989+### Issues (read-only)
9090+9191+- List issues for a repo with state filter (open/closed)
9292+- Issue detail: title, body, author
9393+- Issue comments (threaded — `replyTo` field)
9494+9595+## Caching Strategy
9696+9797+| Data | Stale time | Cache time |
9898+| ------------- | ---------- | ---------- |
9999+| Repo metadata | 5 min | 30 min |
100100+| File tree | 2 min | 10 min |
101101+| File content | 5 min | 30 min |
102102+| Commit log | 2 min | 10 min |
103103+| Profile | 10 min | 60 min |
104104+| README | 5 min | 30 min |
105105+106106+Use TanStack Query's `staleTime` and `gcTime`. Add a query persister (IndexedDB-backed) for offline reads.
107107+108108+## Error Handling
109109+110110+Normalize these failure modes at the service layer:
111111+112112+- Network unreachable → offline banner, serve from cache
113113+- 404 from knot → "Repository not found" or "File not found"
114114+- XRPC error responses → map to typed app errors
115115+- Malformed response → log + generic error state
+75
docs/specs/phase-3.md
···11+# Phase 3 — Search & Activity Feed
22+33+## Goal
44+55+Add repository/user search and a public activity feed so unauthenticated users can discover content and follow what's happening across Tangled.
66+77+## Search
88+99+### Discovery Problem
1010+1111+Tangled's appview serves HTML — there is no documented public JSON search API. Search implementation must be validated against one of these strategies:
1212+1313+1. **Appview JSON endpoint** — check if `tangled.org` exposes a search query endpoint (undocumented but possible)
1414+2. **AT Protocol relay/firehose indexing** — build a lightweight search index from ingested records (requires backend)
1515+3. **Client-side PDS enumeration** — impractical at scale
1616+4. **Scrape appview HTML** — fragile, last resort
1717+1818+**Recommended approach**: Start with strategy 1 (probe for JSON endpoints). If unavailable, implement curated discovery (trending, recent) from cached data and defer full search to Phase 6 with a backend.
1919+2020+### Search UI
2121+2222+- Search bar at top of Explore tab
2323+- Segmented results: Repos | Users
2424+- Recent searches (persisted locally)
2525+- Debounced input (300ms)
2626+- Empty state with suggested queries
2727+2828+### Discovery Sections (fallback if search API unavailable)
2929+3030+- Trending repos (most stars in recent window)
3131+- Recently created repos
3232+- Active repos (recent commits)
3333+- Suggested users
3434+3535+## Activity Feed
3636+3737+### Data Source
3838+3939+Activity is derived from AT Protocol records created by users. The appview's `/timeline` page shows this data. Options for the mobile client:
4040+4141+1. **Appview timeline endpoint** — check if there's a JSON variant
4242+2. **Jetstream subscription** — `@atcute/jetstream` can subscribe to the AT Protocol event stream and filter for `sh.tangled.*` record types
4343+3. **PDS record queries** — poll known users' PDS for recent records
4444+4545+**Recommended approach**: Try option 1 first. Fall back to option 2 (Jetstream) for a real-time feed. Option 3 is too slow for a general feed.
4646+4747+### Feed Item Types
4848+4949+Map these AT Protocol record creations to activity cards:
5050+5151+| Record Type | Activity Kind | Display |
5252+| -------------------------------------- | ------------- | -------------------------------- |
5353+| `sh.tangled.repo` created | repo_created | "{actor} created {repo}" |
5454+| `sh.tangled.feed.star` created | repo_starred | "{actor} starred {repo}" |
5555+| `sh.tangled.graph.follow` created | user_followed | "{actor} followed {target}" |
5656+| `sh.tangled.repo.pull` created | pr_opened | "{actor} opened PR on {repo}" |
5757+| `sh.tangled.repo.pull.status` → merged | pr_merged | "{actor} merged PR on {repo}" |
5858+| `sh.tangled.repo.issue` created | issue_opened | "{actor} opened issue on {repo}" |
5959+| `sh.tangled.repo.issue.state` → closed | issue_closed | "{actor} closed issue on {repo}" |
6060+| `sh.tangled.feed.reaction` created | reaction | "{actor} reacted to {target}" |
6161+6262+### Feed UI
6363+6464+- Filter chips: All, Repos, PRs, Issues, Social
6565+- Infinite scroll with cursor-based pagination
6666+- Pull-to-refresh
6767+- Activity cards: actor avatar + verb + target + relative timestamp
6868+- Tap card → navigate to repo/profile/PR/issue detail
6969+7070+### Feed Caching
7171+7272+- Cache last 100 feed items in IndexedDB
7373+- Show cached feed immediately, refresh in background (stale-while-revalidate)
7474+- Stale time: 1 min
7575+- Persist across app restarts
+167
docs/specs/phase-4.md
···11+# Phase 4 — OAuth & Social Features
22+33+## Goal
44+55+Add AT Protocol OAuth sign-in and authenticated social actions: follow, star, react. Signed-in users get a personalized feed.
66+77+## Authentication
88+99+### Package
1010+1111+`@atcute/oauth-browser-client` ^3.0.0 — minimal browser OAuth client for AT Protocol.
1212+1313+### OAuth Flow
1414+1515+1. User enters handle or DID
1616+2. Resolve handle → DID → PDS → authorization server metadata
1717+3. Initiate OAuth with PKCE + DPoP (P-256)
1818+4. Redirect to authorization server
1919+5. Callback with auth code
2020+6. Exchange code for access + refresh tokens
2121+7. Store session, bind to XRPC client
2222+2323+### Key Functions
2424+2525+| Function | Purpose |
2626+| -------------------------- | ------------------------------------------------------------ |
2727+| `configureOAuth(opts)` | One-time setup: client metadata URL, redirect URI |
2828+| `getSession(did)` | Resume existing session (returns `Session` with `dpopFetch`) |
2929+| `listStoredSessions()` | List all stored accounts |
3030+| `deleteStoredSession(did)` | Remove stored session |
3131+3232+### Session Object
3333+3434+A `Session` provides:
3535+3636+- `did` — authenticated user's DID
3737+- `dpopFetch` — a `fetch` wrapper that auto-attaches DPoP + access token headers
3838+- Token refresh is handled internally
3939+4040+### Client Metadata
4141+4242+The mobile app needs its own OAuth client metadata hosted at a public URL:
4343+4444+```json
4545+{
4646+ "client_id": "https://your-app-domain/oauth/client-metadata.json",
4747+ "client_name": "Twisted",
4848+ "client_uri": "https://your-app-domain",
4949+ "redirect_uris": ["https://your-app-domain/oauth/callback"],
5050+ "grant_types": ["authorization_code", "refresh_token"],
5151+ "response_types": ["code"],
5252+ "token_endpoint_auth_method": "none",
5353+ "application_type": "web",
5454+ "dpop_bound_access_tokens": true,
5555+ "scope": "atproto repo:sh.tangled.graph.follow repo:sh.tangled.feed.star repo:sh.tangled.feed.reaction repo:sh.tangled.actor.profile"
5656+}
5757+```
5858+5959+Request only the scopes needed for Phase 4 social features. Expand scopes in later phases as write features are added.
6060+6161+### Capacitor Considerations
6262+6363+- Web: standard redirect flow works
6464+- iOS/Android via Capacitor: use `App.addListener('appUrlOpen')` to capture the OAuth callback via deep link or custom URL scheme
6565+- Session storage: abstract behind `core/storage/` — use `localStorage` on web, Capacitor Secure Storage plugin on native
6666+6767+### Auth State Machine
6868+6969+```sh
7070+idle → authenticating → authenticated
7171+ → error
7272+authenticated → refreshing → authenticated
7373+ → expired → idle
7474+authenticated → logging_out → idle
7575+```
7676+7777+Store in Pinia (`core/auth/`). Expose via `useAuth()` composable.
7878+7979+## Social Actions
8080+8181+All social actions create or delete AT Protocol records on the user's PDS via the XRPC `com.atproto.repo.createRecord` / `com.atproto.repo.deleteRecord` procedures. The `dpopFetch` from the session handles auth.
8282+8383+### Star a Repo
8484+8585+Create record:
8686+8787+```json
8888+{
8989+ "repo": "did:plc:user",
9090+ "collection": "sh.tangled.feed.star",
9191+ "record": {
9292+ "$type": "sh.tangled.feed.star",
9393+ "subject": "at://did:plc:owner/sh.tangled.repo/tid",
9494+ "createdAt": "2026-03-22T00:00:00Z"
9595+ }
9696+}
9797+```
9898+9999+Unstar: delete the record by its `rkey`.
100100+101101+### Follow a User
102102+103103+Create record:
104104+105105+```json
106106+{
107107+ "repo": "did:plc:user",
108108+ "collection": "sh.tangled.graph.follow",
109109+ "record": {
110110+ "$type": "sh.tangled.graph.follow",
111111+ "subject": "did:plc:target",
112112+ "createdAt": "2026-03-22T00:00:00Z"
113113+ }
114114+}
115115+```
116116+117117+Unfollow: delete the record by its `rkey`.
118118+119119+### React to Content
120120+121121+Create record:
122122+123123+```json
124124+{
125125+ "repo": "did:plc:user",
126126+ "collection": "sh.tangled.feed.reaction",
127127+ "record": {
128128+ "$type": "sh.tangled.feed.reaction",
129129+ "subject": "at://did:plc:owner/sh.tangled.repo.pull/tid",
130130+ "reaction": "thumbsup",
131131+ "createdAt": "2026-03-22T00:00:00Z"
132132+ }
133133+}
134134+```
135135+136136+Available reactions: `thumbsup`, `thumbsdown`, `laugh`, `tada`, `confused`, `heart`, `rocket`, `eyes`.
137137+138138+### Optimistic Updates
139139+140140+All mutations use TanStack Query's `useMutation` with optimistic updates:
141141+142142+1. Immediately update the cache (star count +1, follow state toggled)
143143+2. Fire the mutation
144144+3. On error, roll back the cache and show a toast
145145+146146+## Personalized Feed
147147+148148+When signed in, the Activity tab shows a filtered feed based on:
149149+150150+- Users the signed-in user follows
151151+- Repos the signed-in user has starred
152152+153153+Implementation depends on what the appview provides. If no personalized endpoint exists, filter the global feed client-side based on the user's follow/star records.
154154+155155+## Profile Tab (Authenticated)
156156+157157+When signed in, the Profile tab shows:
158158+159159+- User's avatar, handle, bio, location, pronouns, links
160160+- Pinned repos
161161+- Stats (selected from: merged PRs, open PRs, open issues, repo count, star count)
162162+- Starred repos list
163163+- Following/followers lists
164164+- Edit profile (avatar, bio, links, pinned repos)
165165+- Settings
166166+- Logout
167167+- Account switcher (multiple account support via `listStoredSessions`)
+73
docs/specs/phase-5.md
···11+# Phase 5 — Offline & Performance Polish
22+33+## Goal
44+55+Make the app feel native. Cached data loads instantly, offline mode is graceful, and navigation is smooth on mid-range devices.
66+77+## Offline Strategy
88+99+### Query Persistence
1010+1111+Use TanStack Query's `persistQueryClient` with an IndexedDB adapter:
1212+1313+- Persist all query cache to IndexedDB on each update (debounced)
1414+- On app launch, hydrate TanStack Query cache from IndexedDB before rendering
1515+- Stale-while-revalidate: show persisted data immediately, refresh in background
1616+1717+### What to Persist
1818+1919+| Data | Max cached items | TTL |
2020+| ------------------------------ | ---------------- | ------ |
2121+| Repo metadata | 200 | 7 days |
2222+| File trees | 50 | 3 days |
2323+| File content (recently viewed) | 100 | 3 days |
2424+| README content | 100 | 7 days |
2525+| User profiles | 100 | 7 days |
2626+| Activity feed pages | 10 pages | 1 day |
2727+| Search results | 20 queries | 1 day |
2828+2929+### Offline Detection
3030+3131+- Listen to `navigator.onLine` + `online`/`offline` events
3232+- Show a persistent banner when offline: "You're offline — showing cached data"
3333+- Disable mutation buttons (star, follow) when offline
3434+- Queue mutations for retry when back online (optional, simple queue)
3535+3636+### Sensitive Data
3737+3838+- Auth tokens: Capacitor Secure Storage on native, encrypted `localStorage` wrapper on web
3939+- Never persist tokens in IndexedDB alongside query cache
4040+- Clear auth storage on logout
4141+4242+## Performance Optimizations
4343+4444+### Navigation
4545+4646+- Prefetch repo detail data on repo card hover/long-press
4747+- Keep previous tab's scroll position and data in memory (Ionic's `ion-router-outlet` + `keep-alive`)
4848+- Use `<ion-virtual-scroll>` or a virtualized list for long lists (repos, activity feed)
4949+5050+### Images
5151+5252+- Lazy-load avatars with `loading="lazy"` or Intersection Observer
5353+- Use `avatar.tangled.sh` CDN URLs with size params if available
5454+- Placeholder avatar component with initials fallback
5555+5656+### Bundle
5757+5858+- Route-level code splitting per feature folder
5959+- Tree-shake unused Ionic components
6060+- Measure and optimize with Lighthouse
6161+6262+### Rendering
6363+6464+- Skeleton screens for every data-driven view (already built in Phase 1)
6565+- Debounce search input (already in Phase 3)
6666+- Throttle scroll-based pagination triggers
6767+6868+## Testing Focus
6969+7070+- Offline → online transition: verify data refreshes without duplicates
7171+- Large repo file trees: ensure virtual scroll handles 1000+ items
7272+- Low-bandwidth simulation: verify skeleton → content transitions
7373+- Memory pressure: verify cache eviction works and app doesn't grow unbounded
+75
docs/specs/phase-6.md
···11+# Phase 6 — Write Features & Backend Adapter
22+33+## Goal
44+55+Add authenticated write operations (create issues, comment on PRs/issues, edit profile) and introduce a thin backend (BFF) for operations that don't work well from a pure SPA.
66+77+## Why a Backend
88+99+Some operations are awkward or unsafe from a browser client:
1010+1111+- **Token hardening**: DPoP keys in browser storage are less secure than server-held credentials
1212+- **Unstable procedures**: Tangled's API may change — a backend adapter isolates the mobile client from churn
1313+- **Push notifications**: require server-side registration and delivery
1414+- **Personalized feeds**: server-side aggregation is more efficient than client-side filtering
1515+- **Search**: if no public JSON search API exists, the backend can index and serve search results
1616+- **Rate limiting**: backend can batch and deduplicate requests
1717+1818+### BFF Scope
1919+2020+Thin adapter — not a full backend. Proxies and transforms Tangled/AT Protocol calls.
2121+2222+| Endpoint | Purpose |
2323+| ------------------------------------ | --------------------------------------------------- |
2424+| `POST /auth/session` | OAuth token exchange and session management |
2525+| `GET /feed/personalized` | Pre-filtered activity feed for the user |
2626+| `GET /search/repos`, `/search/users` | Search proxy/index |
2727+| `POST /notifications/register` | Push notification device registration |
2828+| Passthrough for stable XRPC calls | Avoid duplicating what the client already does well |
2929+3030+## Write Features
3131+3232+### Create Issue
3333+3434+- Screen: issue creation form within repo detail
3535+- Fields: title (required), body (markdown), mentions
3636+- Creates `sh.tangled.repo.issue` record on user's PDS
3737+- Optimistic: add to local issue list, remove on failure
3838+3939+### Comment on Issue / PR
4040+4141+- Screen: comment input at bottom of issue/PR detail
4242+- Creates `sh.tangled.repo.issue.comment` or `sh.tangled.repo.pull.comment` record
4343+- Supports `replyTo` for threaded issue comments
4444+- Supports `mentions` (DID array) and `references` (AT-URI array)
4545+4646+### Edit Profile
4747+4848+- Screen: profile edit form
4949+- Updates `sh.tangled.actor.profile` record (key: `self`)
5050+- Fields: avatar (image upload, max 1MB, png/jpeg), bio (max 256 graphemes), links (max 5 URIs), location (max 40 graphemes), pronouns (max 40 chars), pinned repos (max 6 AT-URIs), display stats (max 2 from: merged-pr-count, closed-pr-count, open-pr-count, open-issue-count, closed-issue-count, repo-count, star-count), bluesky cross-posting toggle
5151+5252+### Issue State Management
5353+5454+- Close/reopen issues by creating `sh.tangled.repo.issue.state` records
5555+- State values: `sh.tangled.repo.issue.state.open`, `sh.tangled.repo.issue.state.closed`
5656+5757+## Push Notifications
5858+5959+- Register device token with BFF
6060+- BFF subscribes to Jetstream for the user's relevant events
6161+- Deliver via APNs (iOS) / FCM (Android)
6262+- Notification types: PR activity on your repos, issue comments, new followers, stars
6363+6464+## Expanded OAuth Scopes
6565+6666+Phase 6 requires additional scopes beyond Phase 4:
6767+6868+```sh
6969+repo:sh.tangled.repo.issue
7070+repo:sh.tangled.repo.issue.comment
7171+repo:sh.tangled.repo.issue.state
7272+repo:sh.tangled.repo.pull.comment
7373+```
7474+7575+Handle scope upgrades gracefully — re-authorize if the user's existing session lacks required scopes.
+85
docs/specs/phase-7.md
···11+# Phase 7 — Real-Time Feed & Advanced Features
22+33+## Goal
44+55+Add real-time event streaming, custom feed logic, and advanced social coding features. This phase makes the app feel alive.
66+77+## Jetstream Integration
88+99+### Package
1010+1111+`@atcute/jetstream` — subscribe to the AT Protocol event stream.
1212+1313+### Architecture
1414+1515+Connect to a Jetstream relay and filter for `sh.tangled.*` collections:
1616+1717+```sh
1818+Jetstream WebSocket
1919+ → filter: sh.tangled.* events
2020+ → normalize into ActivityItem
2121+ → merge into TanStack Query feed cache
2222+ → reactive UI update
2323+```
2424+2525+### Connection Management
2626+2727+- Connect on app foreground, disconnect on background
2828+- Reconnect with exponential backoff
2929+- Track cursor position for gap-fill on reconnect
3030+- Battery-aware: reduce polling frequency on low battery (Capacitor Battery API)
3131+3232+### Live Indicators
3333+3434+- Repo detail: show "new commits" banner when ref updates arrive
3535+- Activity feed: show "X new items" pill, tap to scroll to top and reveal
3636+- PR detail: live status updates (open → merged)
3737+3838+## Custom Feeds
3939+4040+Allow users to create saved feed configurations:
4141+4242+- "My repos" — activity on repos I own
4343+- "Watching" — activity on repos I starred
4444+- "Team" — activity from users I follow
4545+- Custom filters: by repo, by user, by event type
4646+4747+Feeds are stored locally in IndexedDB. If a BFF exists, they can optionally sync server-side for push notification filtering.
4848+4949+## Advanced Features
5050+5151+### Repo Forking
5252+5353+- Fork button on repo detail (requires `rpc:sh.tangled.repo.create` scope)
5454+- Fork status indicator via `sh.tangled.repo.forkStatus` (up-to-date, fast-forwardable, conflict, missing branch)
5555+- Sync fork via `sh.tangled.repo.forkSync`
5656+5757+### Label Support
5858+5959+- Display labels on issues and PRs
6060+- Apply/remove labels (requires label scopes)
6161+- Color-coded label chips
6262+6363+### Reaction Picker
6464+6565+- Expand reaction support beyond star/follow
6666+- Emoji picker for: thumbsup, thumbsdown, laugh, tada, confused, heart, rocket, eyes
6767+- Show reaction counts on PRs, issues, comments
6868+6969+### PR Interdiff
7070+7171+- View diff between PR rounds (round N vs round N+1)
7272+- Useful for code review on mobile — see what changed since last review
7373+7474+### Knot Information
7575+7676+- Show which knot hosts a repo
7777+- Knot version and status
7878+- Useful for debugging and transparency
7979+8080+## Testing
8181+8282+- WebSocket reliability under network transitions (WiFi → cellular)
8383+- Feed deduplication when Jetstream replays events
8484+- Memory usage with long-running WebSocket connections
8585+- Battery impact measurement on mobile
+64
docs/tasks/phase-1.md
···11+# Phase 1 Tasks — Project Shell & Design System
22+33+## Scaffold
44+55+- [ ] Create Ionic Vue project with TypeScript (`ionic start twisted tabs --type vue`)
66+- [ ] Configure Capacitor for iOS and Android
77+- [ ] Set up path aliases (`@/` → `src/`)
88+- [ ] Install and configure Pinia
99+- [ ] Install and configure TanStack Query for Vue
1010+- [ ] Create the directory structure per spec (`app/`, `core/`, `services/`, `domain/`, `features/`, `components/`)
1111+1212+## Routing & Navigation
1313+1414+- [ ] Define five-tab layout: Home, Explore, Activity, Profile (visible tabs) + Repo (pushed route)
1515+- [ ] Configure Vue Router with Ionic tab routing
1616+- [ ] Add route definitions for all Phase 1 placeholder pages
1717+- [ ] Verify tab-to-tab navigation preserves scroll position and component state
1818+1919+## Domain Models
2020+2121+- [ ] Create `domain/models/user.ts` — `UserSummary` type
2222+- [ ] Create `domain/models/repo.ts` — `RepoSummary`, `RepoDetail`, `RepoFile` types
2323+- [ ] Create `domain/models/pull-request.ts` — `PullRequestSummary` type
2424+- [ ] Create `domain/models/issue.ts` — `IssueSummary` type
2525+- [ ] Create `domain/models/activity.ts` — `ActivityItem` type
2626+2727+## Mock Data
2828+2929+- [ ] Create `src/mocks/users.ts` — factory for `UserSummary` instances
3030+- [ ] Create `src/mocks/repos.ts` — factory for `RepoSummary` and `RepoDetail` instances
3131+- [ ] Create `src/mocks/pull-requests.ts` — factory for `PullRequestSummary` instances
3232+- [ ] Create `src/mocks/issues.ts` — factory for `IssueSummary` instances
3333+- [ ] Create `src/mocks/activity.ts` — factory for `ActivityItem` instances
3434+- [ ] Use realistic data: handles like `alice.tngl.sh`, repo names, timestamps within last 30 days
3535+3636+## Design System Components
3737+3838+- [ ] `components/common/RepoCard.vue` — compact repo summary (name, owner, description, language, stars)
3939+- [ ] `components/common/UserCard.vue` — avatar + handle + bio snippet
4040+- [ ] `components/common/ActivityCard.vue` — icon + actor + verb + target + relative timestamp
4141+- [ ] `components/common/EmptyState.vue` — icon + message + optional action button
4242+- [ ] `components/common/ErrorBoundary.vue` — catch errors, show retry UI
4343+- [ ] `components/common/SkeletonLoader.vue` — shimmer placeholders (variants: card, list-item, profile)
4444+- [ ] `components/repo/FileTreeItem.vue` — file/dir icon + name
4545+- [ ] `components/repo/MarkdownRenderer.vue` — render markdown to HTML (stub with basic styling)
4646+4747+## Feature Pages (placeholder with mock data)
4848+4949+- [ ] `features/home/HomePage.vue` — trending repos list, recent activity list
5050+- [ ] `features/explore/ExplorePage.vue` — search bar (non-functional), repo/user tabs, repo list
5151+- [ ] `features/repo/RepoDetailPage.vue` — segmented layout: Overview, Files, Issues, PRs
5252+- [ ] `features/repo/RepoOverview.vue` — header, description, README placeholder, stats
5353+- [ ] `features/repo/RepoFiles.vue` — file tree list from mock data
5454+- [ ] `features/repo/RepoIssues.vue` — issue list from mock data
5555+- [ ] `features/repo/RepoPRs.vue` — PR list from mock data
5656+- [ ] `features/activity/ActivityPage.vue` — filter chips + activity card list
5757+- [ ] `features/profile/ProfilePage.vue` — sign-in prompt (unauthenticated state)
5858+5959+## Quality
6060+6161+- [ ] Verify all pages render with skeleton loaders before mock data appears
6262+- [ ] Verify tab switches don't cause layout shift
6363+- [ ] Run Lighthouse on the web build — target first-paint under 2s
6464+- [ ] Verify iOS and Android builds compile and launch via Capacitor
+67
docs/tasks/phase-2.md
···11+# Phase 2 Tasks — Public Tangled Browsing
22+33+## Protocol Setup
44+55+- [ ] Install `@atcute/client` and `@atcute/tangled`
66+- [ ] Create `services/atproto/client.ts` — singleton XRPC client with configurable base URL
77+- [ ] Add error interceptor that normalizes XRPC errors into typed app errors
88+- [ ] Create `core/errors/tangled.ts` — error types: NotFound, NetworkError, MalformedResponse, RateLimited
99+1010+## API Validation
1111+1212+- [ ] Probe `tangled.org` for JSON API endpoints (check headers, try `Accept: application/json`)
1313+- [ ] Confirm knot XRPC endpoints work from browser (CORS check against `us-west.tangled.sh`)
1414+- [ ] Document which data comes from knots vs appview vs PDS
1515+- [ ] Test `com.atproto.repo.getRecord` for fetching user profiles and repo records from PDS
1616+1717+## Service Layer
1818+1919+- [ ] Create `services/tangled/endpoints.ts` — typed wrappers for each XRPC query
2020+- [ ] Create `services/tangled/normalizers.ts` — transform raw responses → domain models
2121+- [ ] Create `services/tangled/queries.ts` — TanStack Query hooks with cache keys and stale times
2222+- [ ] Implement knot routing: determine correct knot hostname for a given repo
2323+2424+## Repository Browsing
2525+2626+- [ ] Wire `RepoDetailPage` to live repo data (metadata from PDS record + git data from knot)
2727+- [ ] Implement repo overview: description, topics, default branch, language breakdown
2828+- [ ] Implement README fetch: `sh.tangled.repo.blob` for `README.md` on default branch
2929+- [ ] Wire `MarkdownRenderer` to render real README content
3030+- [ ] Implement file tree: `sh.tangled.repo.tree` → navigate directories
3131+- [ ] Implement file viewer: `sh.tangled.repo.blob` → syntax-highlighted display
3232+- [ ] Implement commit log: `sh.tangled.repo.log` with cursor pagination
3333+- [ ] Implement branch list: `sh.tangled.repo.branches`
3434+3535+## Profile Browsing
3636+3737+- [ ] Fetch user profile from PDS: `com.atproto.repo.getRecord` for `sh.tangled.actor.profile`
3838+- [ ] Display profile: avatar (via `avatar.tangled.sh`), bio, links, location, pronouns, pinned repos
3939+- [ ] List user's repos: fetch `sh.tangled.repo` records from user's PDS
4040+- [ ] Wire `UserCard` component to real data
4141+4242+## Issues (read-only)
4343+4444+- [ ] Fetch issues for a repo from PDS records
4545+- [ ] Display issue list with state filter (open/closed)
4646+- [ ] Issue detail view: title, body, author, state
4747+- [ ] Issue comments: fetch `sh.tangled.repo.issue.comment` records, render threaded
4848+4949+## Pull Requests (read-only)
5050+5151+- [ ] Fetch PRs for a repo from PDS records
5252+- [ ] Display PR list with status filter (open/closed/merged)
5353+- [ ] PR detail view: title, body, author, source/target branches
5454+- [ ] PR comments: fetch `sh.tangled.repo.pull.comment` records
5555+5656+## Caching
5757+5858+- [ ] Configure TanStack Query stale/gc times per data type (see spec)
5959+- [ ] Set up IndexedDB query persister for offline reads
6060+- [ ] Verify stale-while-revalidate behavior: cached data shows immediately, refreshes in background
6161+6262+## Quality
6363+6464+- [ ] Replace all mock data usage with live queries (remove or gate mocks behind a flag)
6565+- [ ] Test with real Tangled repos (e.g., `tangled.org/core`)
6666+- [ ] Verify error states render correctly: 404, network failure, empty repos
6767+- [ ] Test on slow network (throttled devtools) — verify skeleton → content transition
+60
docs/tasks/phase-3.md
···11+# Phase 3 Tasks — Search & Activity Feed
22+33+## Search API Discovery
44+55+- [ ] Probe `tangled.org` for search endpoints (try `/search?q=`, `/api/search`, check network tab on live site)
66+- [ ] If JSON search exists, document the request/response format
77+- [ ] If no JSON search, decide: curated discovery now + backend search later, or HTML scraping
88+99+## Search Implementation
1010+1111+- [ ] Create `services/tangled/search.ts` — search service (real endpoint or fallback)
1212+- [ ] Implement debounced search input (300ms) in Explore tab
1313+- [ ] Implement segmented search results: Repos tab, Users tab
1414+- [ ] Implement search result rendering with `RepoCard` and `UserCard`
1515+- [ ] Implement empty search state with suggestions
1616+- [ ] Persist recent searches in local storage (max 20)
1717+- [ ] Clear search history action
1818+1919+## Discovery Sections (if search API unavailable)
2020+2121+- [ ] Implement "Trending repos" section (source TBD — may require appview scraping or curated list)
2222+- [ ] Implement "Recently created repos" section
2323+- [ ] Implement "Suggested users" section
2424+- [ ] Wire discovery sections into Home and Explore tabs
2525+2626+## Activity Feed — Data Source
2727+2828+- [ ] Investigate `tangled.org/timeline` for JSON variant (check with Accept headers)
2929+- [ ] If no JSON timeline, evaluate `@atcute/jetstream` for real-time feed
3030+- [ ] If neither works, implement polling-based feed from known users' PDS records
3131+- [ ] Document chosen approach and any limitations
3232+3333+## Activity Feed — Implementation
3434+3535+- [ ] Create `services/tangled/feed.ts` — feed data source
3636+- [ ] Create normalizer: raw AT Protocol events → `ActivityItem` domain model
3737+- [ ] Implement `ActivityPage` with real feed data
3838+- [ ] Implement filter chips: All, Repos, PRs, Issues, Social
3939+- [ ] Implement infinite scroll with cursor-based pagination
4040+- [ ] Implement pull-to-refresh
4141+- [ ] Implement tap-to-navigate: activity card → repo/profile/PR/issue detail
4242+4343+## Feed Caching
4444+4545+- [ ] Cache last 100 feed items in IndexedDB via query persister
4646+- [ ] Show cached feed immediately on tab switch
4747+- [ ] Stale time: 1 minute
4848+- [ ] Verify feed persists across app restarts
4949+5050+## Home Tab
5151+5252+- [ ] Wire Home tab to real data: trending repos + recent activity
5353+- [ ] Add "personalized" section placeholder (shows sign-in prompt when unauthenticated)
5454+5555+## Quality
5656+5757+- [ ] Test search with various queries — verify results are relevant
5858+- [ ] Test activity feed with pull-to-refresh and pagination
5959+- [ ] Test offline: cached feed shows, search degrades gracefully
6060+- [ ] Verify no duplicate items in feed after refresh
+84
docs/tasks/phase-4.md
···11+# Phase 4 Tasks — OAuth & Social Features
22+33+## OAuth Setup
44+55+- [ ] Install `@atcute/oauth-browser-client`
66+- [ ] Host OAuth client metadata JSON at a public URL (or configure for local dev)
77+- [ ] Create `core/auth/oauth.ts` — call `configureOAuth()` with client metadata URL and redirect URI
88+- [ ] Create `core/auth/session.ts` — session management: get, list, delete stored sessions
99+- [ ] Create `core/auth/store.ts` — Pinia auth store with state machine (idle → authenticating → authenticated → error)
1010+1111+## Login Flow
1212+1313+- [ ] Create `features/profile/LoginPage.vue` — handle input field + "Sign in" button
1414+- [ ] Implement handle → DID resolution
1515+- [ ] Implement OAuth redirect initiation
1616+- [ ] Create `/oauth/callback` route to handle redirect back
1717+- [ ] Implement token exchange on callback
1818+- [ ] Store session and update auth store
1919+- [ ] Redirect to Profile tab after successful login
2020+- [ ] Handle auth errors: invalid handle, OAuth denied, network failure
2121+2222+## Session Management
2323+2424+- [ ] Implement session restoration on app launch (call `getSession()` for stored DID)
2525+- [ ] Implement automatic token refresh (handled by `@atcute/oauth-browser-client` internally)
2626+- [ ] Implement logout: clear session, reset auth store, redirect to Home
2727+- [ ] Implement account switcher: `listStoredSessions()`, switch between accounts
2828+2929+## Capacitor Deep Links
3030+3131+- [ ] Configure custom URL scheme for OAuth callback on iOS/Android
3232+- [ ] Add `App.addListener('appUrlOpen')` handler to capture callback
3333+- [ ] Test OAuth flow on iOS simulator and Android emulator
3434+3535+## Auth-Aware XRPC Client
3636+3737+- [ ] Create authenticated XRPC client that uses `session.dpopFetch` for requests
3838+- [ ] Service layer: use authenticated client for mutations, public client for queries
3939+- [ ] Handle 401/expired session: trigger re-auth flow
4040+4141+## Social Actions — Star
4242+4343+- [ ] Create `services/tangled/mutations.ts` — mutation functions
4444+- [ ] Implement `starRepo(repoAtUri)` — creates `sh.tangled.feed.star` record on user's PDS
4545+- [ ] Implement `unstarRepo(rkey)` — deletes star record
4646+- [ ] Add star/unstar button to `RepoDetailPage` overview
4747+- [ ] Optimistic update: toggle star state and count immediately, rollback on error
4848+- [ ] Track user's existing stars to show correct initial state
4949+5050+## Social Actions — Follow
5151+5252+- [ ] Implement `followUser(targetDid)` — creates `sh.tangled.graph.follow` record
5353+- [ ] Implement `unfollowUser(rkey)` — deletes follow record
5454+- [ ] Add follow/unfollow button to profile pages and user cards
5555+- [ ] Optimistic update: toggle follow state immediately
5656+- [ ] Track user's existing follows to show correct initial state
5757+5858+## Social Actions — React
5959+6060+- [ ] Implement `addReaction(subjectUri, reaction)` — creates `sh.tangled.feed.reaction` record
6161+- [ ] Implement `removeReaction(rkey)` — deletes reaction record
6262+- [ ] Add reaction button/picker to PR and issue detail views
6363+- [ ] Show reaction counts grouped by type
6464+6565+## Personalized Feed
6666+6767+- [ ] When signed in, filter activity feed to show activity from followed users and starred repos
6868+- [ ] Add "For You" / "Global" toggle on Activity tab
6969+- [ ] If appview provides a personalized endpoint, use it; otherwise filter client-side
7070+7171+## Profile Tab (Authenticated)
7272+7373+- [ ] Wire Profile tab to show current user's profile data
7474+- [ ] Show pinned repos, stats, starred repos, following list
7575+- [ ] Add logout button
7676+- [ ] Add account switcher UI
7777+7878+## Quality
7979+8080+- [ ] Test full OAuth flow: login → browse → star → follow → logout
8181+- [ ] Test session restoration after app restart
8282+- [ ] Test on web, iOS simulator, Android emulator
8383+- [ ] Test error cases: denied OAuth, expired session, failed mutation
8484+- [ ] Verify optimistic updates roll back correctly on mutation failure
+64
docs/tasks/phase-5.md
···11+# Phase 5 Tasks — Offline & Performance Polish
22+33+## Query Persistence
44+55+- [ ] Set up `persistQueryClient` with IndexedDB adapter
66+- [ ] Configure persistence: debounced writes, max cache size, TTL per data type
77+- [ ] Hydrate query cache from IndexedDB before first render
88+- [ ] Verify: kill app → relaunch → cached data appears immediately without network
99+1010+## Offline Detection
1111+1212+- [ ] Create `core/network/status.ts` — reactive online/offline state (composable)
1313+- [ ] Show persistent offline banner when `navigator.onLine` is false
1414+- [ ] Disable mutation buttons (star, follow, react) when offline
1515+- [ ] Show toast when network returns: "Back online — refreshing"
1616+1717+## Secure Storage
1818+1919+- [ ] Abstract auth token storage behind `core/storage/secure.ts`
2020+- [ ] Web: encrypted localStorage wrapper
2121+- [ ] Native: Capacitor Secure Storage plugin
2222+- [ ] Verify tokens are never stored in IndexedDB query cache
2323+- [ ] Clear secure storage on logout
2424+2525+## Cache Eviction
2626+2727+- [ ] Implement max-item limits per data type (repos: 200, files: 100, profiles: 100)
2828+- [ ] Implement TTL eviction (remove entries older than their configured TTL)
2929+- [ ] Run eviction on app launch and periodically (every 30 min)
3030+- [ ] Measure IndexedDB size and log warnings if approaching limits
3131+3232+## Navigation Performance
3333+3434+- [ ] Verify Ionic `keep-alive` preserves tab state and scroll position
3535+- [ ] Implement data prefetch on repo card visibility (Intersection Observer)
3636+- [ ] Test tab switch speed — should feel instant with cached data
3737+- [ ] Profile and fix any layout shifts during navigation
3838+3939+## List Virtualization
4040+4141+- [ ] Replace flat lists with virtualized scroll for: repo lists, activity feed, file trees
4242+- [ ] Test with 1000+ item lists — verify smooth scrolling
4343+- [ ] Verify scroll position restoration when navigating back
4444+4545+## Image Optimization
4646+4747+- [ ] Lazy-load all avatars
4848+- [ ] Add initials fallback for missing avatars
4949+- [ ] Use appropriate image sizes from `avatar.tangled.sh`
5050+5151+## Bundle Optimization
5252+5353+- [ ] Add route-level code splitting (lazy imports per feature)
5454+- [ ] Tree-shake unused Ionic components (configure Ionic's component imports)
5555+- [ ] Measure bundle size — target under 500KB initial JS
5656+- [ ] Run Lighthouse audit — target 90+ performance score on mobile
5757+5858+## Quality
5959+6060+- [ ] Test offline → online transition: data refreshes without duplicates
6161+- [ ] Test low-bandwidth (3G throttle): skeleton → content transitions are smooth
6262+- [ ] Test memory usage over extended use: navigate many repos, check heap doesn't grow unbounded
6363+- [ ] Test on real iOS and Android devices (not just simulators)
6464+- [ ] Measure and document cold start time, tab switch time, scroll performance
+86
docs/tasks/phase-6.md
···11+# Phase 6 Tasks — Write Features & Backend Adapter
22+33+## Backend (BFF) Setup
44+55+- [ ] Choose runtime (Node/Deno/Bun) and framework (Hono/Fastify/Express)
66+- [ ] Scaffold BFF project with TypeScript
77+- [ ] Implement health check endpoint
88+- [ ] Deploy to hosting (Fly.io, Railway, etc.)
99+- [ ] Configure CORS for the mobile app's origins
1010+1111+## Backend — Auth Proxy
1212+1313+- [ ] Implement OAuth token exchange endpoint (if moving auth server-side)
1414+- [ ] Implement session endpoint that returns user info
1515+- [ ] Decide: keep client-side OAuth or migrate to BFF-mediated auth
1616+1717+## Backend — Search
1818+1919+- [ ] Implement search indexer: subscribe to Jetstream, index `sh.tangled.repo` and `sh.tangled.actor.profile` records
2020+- [ ] Implement `GET /search/repos?q=` endpoint
2121+- [ ] Implement `GET /search/users?q=` endpoint
2222+- [ ] Wire mobile client's search service to BFF endpoints
2323+2424+## Backend — Personalized Feed
2525+2626+- [ ] Implement `GET /feed/personalized` — aggregate activity for the user's follows and stars
2727+- [ ] Index relevant events from Jetstream
2828+- [ ] Wire mobile client's feed to BFF endpoint when authenticated
2929+3030+## Create Issue
3131+3232+- [ ] Create `features/repo/CreateIssuePage.vue` — title + body (markdown) form
3333+- [ ] Implement `createIssue()` mutation in `services/tangled/mutations.ts`
3434+- [ ] Create `sh.tangled.repo.issue` record on user's PDS
3535+- [ ] Optimistic update: add issue to local list
3636+- [ ] Navigate to new issue detail on success
3737+3838+## Comment on Issue
3939+4040+- [ ] Add comment input to issue detail view
4141+- [ ] Implement `createIssueComment()` mutation
4242+- [ ] Create `sh.tangled.repo.issue.comment` record with `replyTo` support for threading
4343+- [ ] Optimistic update: append comment to list
4444+4545+## Comment on PR
4646+4747+- [ ] Add comment input to PR detail view
4848+- [ ] Implement `createPRComment()` mutation
4949+- [ ] Create `sh.tangled.repo.pull.comment` record
5050+5151+## Issue State Management
5252+5353+- [ ] Add close/reopen button to issue detail (author and repo owner only)
5454+- [ ] Implement `closeIssue()` / `reopenIssue()` — create `sh.tangled.repo.issue.state` record
5555+- [ ] Optimistic update: toggle state badge
5656+5757+## Edit Profile
5858+5959+- [ ] Create `features/profile/EditProfilePage.vue`
6060+- [ ] Implement avatar upload (max 1MB, png/jpeg) via blob upload + record update
6161+- [ ] Implement bio edit (max 256 graphemes)
6262+- [ ] Implement links edit (max 5 URIs)
6363+- [ ] Implement location, pronouns, pinned repos, stats selection, bluesky toggle
6464+- [ ] Update `sh.tangled.actor.profile` record (key: `self`)
6565+6666+## Scope Upgrade
6767+6868+- [ ] Detect when user's session lacks scopes needed for write operations
6969+- [ ] Prompt user to re-authorize with expanded scopes
7070+- [ ] Handle scope upgrade flow gracefully (no data loss)
7171+7272+## Push Notifications (if BFF exists)
7373+7474+- [ ] Implement `POST /notifications/register` on BFF — register device token
7575+- [ ] Configure Capacitor Push Notifications plugin
7676+- [ ] Register device token on login
7777+- [ ] BFF: subscribe to Jetstream events relevant to user, deliver via APNs/FCM
7878+- [ ] Handle notification tap → deep link to relevant content
7979+8080+## Quality
8181+8282+- [ ] Test issue creation end-to-end: create → verify on tangled.org
8383+- [ ] Test commenting on issues and PRs
8484+- [ ] Test profile editing: avatar upload, bio change
8585+- [ ] Test scope upgrade flow
8686+- [ ] Verify mutations work offline-queued (if implemented) or show appropriate offline errors
+68
docs/tasks/phase-7.md
···11+# Phase 7 Tasks — Real-Time Feed & Advanced Features
22+33+## Jetstream Integration
44+55+- [ ] Install `@atcute/jetstream`
66+- [ ] Create `services/atproto/jetstream.ts` — WebSocket connection manager
77+- [ ] Filter events for `sh.tangled.*` collections
88+- [ ] Normalize events into `ActivityItem` domain model
99+- [ ] Merge live events into TanStack Query feed cache
1010+- [ ] Implement connection lifecycle: connect on foreground, disconnect on background
1111+- [ ] Implement reconnection with exponential backoff and cursor tracking
1212+- [ ] Add battery-aware throttling (Capacitor Battery API)
1313+1414+## Live UI Indicators
1515+1616+- [ ] Activity feed: "X new items" pill at top, tap to reveal
1717+- [ ] Repo detail: "New commits available" banner on ref update events
1818+- [ ] PR detail: live status badge updates (open → merged)
1919+- [ ] Issue detail: live comment count updates
2020+2121+## Custom Feeds
2222+2323+- [ ] Create `domain/feed/custom-feed.ts` — feed configuration model
2424+- [ ] Implement feed builder UI: name + filter rules (by repo, user, event type)
2525+- [ ] Store custom feeds in IndexedDB
2626+- [ ] Render custom feed as a selectable tab/option on Activity page
2727+- [ ] "My repos" preset, "Watching" preset, "Team" preset
2828+2929+## Repo Forking
3030+3131+- [ ] Add fork button to repo detail (authenticated only)
3232+- [ ] Implement fork creation via `sh.tangled.repo.create` with `source` field
3333+- [ ] Show fork status badge: up-to-date, fast-forwardable, conflict, missing branch
3434+- [ ] Implement "Sync fork" action via `sh.tangled.repo.forkSync`
3535+3636+## Labels
3737+3838+- [ ] Fetch label definitions for a repo
3939+- [ ] Display color-coded label chips on issues and PRs
4040+- [ ] Implement label filtering on issue/PR lists
4141+- [ ] Add/remove labels on issues and PRs (authenticated, with label scopes)
4242+4343+## Expanded Reactions
4444+4545+- [ ] Add reaction picker component: thumbsup, thumbsdown, laugh, tada, confused, heart, rocket, eyes
4646+- [ ] Show grouped reaction counts on PRs, issues, and comments
4747+- [ ] Add/remove reactions with optimistic updates
4848+4949+## PR Interdiff
5050+5151+- [ ] Detect PR round count
5252+- [ ] Add round selector to PR detail
5353+- [ ] Fetch and display diff between selected rounds
5454+- [ ] Use `sh.tangled.repo.compare` for cross-round comparison
5555+5656+## Knot Info
5757+5858+- [ ] Show knot hostname on repo detail
5959+- [ ] Fetch knot version via `sh.tangled.knot.version`
6060+- [ ] Display knot status/health indicator
6161+6262+## Quality
6363+6464+- [ ] Test Jetstream under network transitions (WiFi → cellular → offline → online)
6565+- [ ] Verify no duplicate events after reconnection with cursor
6666+- [ ] Measure battery impact of WebSocket connection on iOS and Android
6767+- [ ] Test memory usage with long-running Jetstream connection
6868+- [ ] Load test: simulate high-frequency events, verify UI stays responsive