diff --git a/CHANGELOG.md b/CHANGELOG.md index 746f1b2..b42fc9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # CHANGELOG -## v0.2.0-pre — 2026-03-02 +## Unreleased + +## v0.2.0 — 2026-03-20 ### Features diff --git a/docs/at-proto.md b/docs/at-proto.md deleted file mode 100644 index 90b614e..0000000 --- a/docs/at-proto.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: AT Protocol Integration Spec -updated: 2026-03-19 ---- - -## Goals - -- Use [jacquard](https://docs.rs/crate/jacquard/latest) (`^0.9`) to handle AT Protocol interactions. -- AT Protocol OAuth with loopback redirect for desktop auth flow. -- Tangled string integration (publish and import documents as strings). -- Standard.Site post integration - -## Tangled Strings - -Strings are Tangled's equivalent of GitHub Gists — lightweight text/code snippets stored as AT Protocol records on the user's PDS under the `sh.tangled.string` collection. The Tangled AppView indexes them via Jetstream ingestion, but all CRUD goes through standard `com.atproto.repo.*` XRPC endpoints on the PDS directly — no Tangled-specific server API is needed for read/write. - -### Constraints - -- **Record size limit:** PDS records are capped at **2 MiB**. Large documents must be rejected with a clear error before attempting upload. -- **Filename:** 1–140 graphemes. Default to the document's current filename. -- **Description:** 0–280 graphemes. User-provided summary. -- **Contents:** min 1 grapheme. The document body (markdown or plaintext). -- **Key format:** TID (timestamp-based, base32-sortkey encoded, e.g. `3jzfcijpj2z2a`). -- **AT URI format:** `at:///sh.tangled.string/` - -### XRPC Endpoints - -All endpoints target `/xrpc/{NSID}` on the user's PDS. Collection is always `"sh.tangled.string"`. - -| Operation | Endpoint | Method | Auth | -| --------- | ------------------------------- | ------ | -------- | -| Create | `com.atproto.repo.createRecord` | POST | Required | -| Read | `com.atproto.repo.getRecord` | GET | No | -| List | `com.atproto.repo.listRecords` | GET | No | -| Update | `com.atproto.repo.putRecord` | POST | Required | -| Delete | `com.atproto.repo.deleteRecord` | POST | Required | -| Batch | `com.atproto.repo.applyWrites` | POST | Required | - -`listRecords` supports cursor-based pagination (`cursor`, `limit`, `reverse`, `rkeyStart`/`rkeyEnd`). Reading records does not require authentication — any user's public strings can be listed and fetched without a session. - -### Data Flow - -```text -┌──────────────┐ Tauri commands ┌───────────────────┐ -│ Frontend │ ───────────────────── │ src-tauri/ │ -│ (React/TS) │ │ commands.rs │ -│ │ ◄── CommandResponse │ + atproto.rs │ -└──────────────┘ └────────┬─────────┘ - │ - jacquard Agent - │ - ┌────────▼─────────┐ - │ User's PDS │ - │ (XRPC endpoints)│ - └──────────────────┘ - │ - Jetstream ingestion - │ - ┌────────▼────────────┐ - │ Tangled AppView │ - │ (tangled.sh) │ - └─────────────────────┘ -``` - -### Jacquard Usage - -Jacquard (`^0.9`, ~99k downloads/month, MPL-2.0) provides generated types for the `sh.tangled.string` lexicon in `jacquard_api::sh_tangled::string`: - -- `TangledString` / `TangledStringBuilder` — record type and builder. -- `TangledStringRecord` — full record with URI and CID. - -Sub-crates: - -| Crate | Purpose | -| ------------------- | --------------------------------------------------------------- | -| `jacquard-common` | Core types: DIDs, handles, AT URIs, NSIDs, TIDs, CIDs | -| `jacquard-api` | 646+ generated lexicon bindings (includes `sh_tangled::string`) | -| `jacquard-oauth` | OAuth/DPoP with loopback server support | -| `jacquard-identity` | Handle/DID resolution, OAuth metadata discovery | - -XRPC calls use the builder + `.send()` pattern: - -```rust -use jacquard::api::com_atproto::repo::create_record::CreateRecordRequest; -use jacquard::api::sh_tangled::string::TangledString; - -let record = TangledString::builder() - .filename("notes.md") - .description("My notes") - .contents(body) - .created_at(chrono::Utc::now()) - .build(); - -let response = CreateRecordRequest::builder() - .repo(&session.did) - .collection("sh.tangled.string") - .record(record) - .build() - .send(&agent) - .await?; -``` - -For reads (no auth needed), use a stateless `reqwest::Client` with the `XrpcExt` trait or an unauthenticated agent. For writes, the `Agent` handles DPoP headers and token refresh automatically. - -Jacquard types use zero-copy deserialization via `CowStr<'_>`. Use `.parse()` for borrowed data or `.into_output()` for owned `'static` data when the response outlives the buffer. - -### Backend Module Structure - -```sh -src-tauri/src/ -├── atproto/ -│ ├── mod.rs # re-exports shared auth types/state -│ ├── auth.rs # OAuth loopback flow, session restore, logout cleanup -│ └── strings.rs # Tangled string listing + fetch helpers -``` - -`AtProtoState` lives inside `AppState` and owns the Jacquard OAuth client plus persisted session metadata paths. The current auth slice restores an existing session during app startup, exposes the active `SessionInfo`, and clears persisted auth artifacts when restoration or logout fails. - -**Tauri commands:** - -| Command | Args | Returns | Auth | -| ------------------------ | -------------------------------------- | -------------------------------------- | --------- | -| `atproto_login` | `handle: String` | `CommandResponse` | Initiates | -| `atproto_logout` | — | `CommandResponse<()>` | Required | -| `atproto_session_status` | — | `CommandResponse>` | No | -| `string_create` | `filename, description, contents` | `CommandResponse` | Required | -| `string_update` | `tid, filename, description, contents` | `CommandResponse` | Required | -| `string_delete` | `tid` | `CommandResponse<()>` | Required | -| `string_list` | `did_or_handle` | `CommandResponse>` | No | -| `string_get` | `did_or_handle, tid` | `CommandResponse` | No | - -`StringRecord` contains: `uri` (AT URI), `tid`, `filename`, `description`, `contents`, `created_at`. - -### Frontend Structure - -```sh -src/ -├── state/stores/ui.ts # auth/import sheet mode + hydrated/pending/session state -├── state/selectors.ts # AT Protocol selector hooks -├── ports/commands.ts # atproto_login / logout / session_status / string_list / string_get -├── hooks/controllers/ -│ └── useAtProtoController.ts -├── components/ -│ ├── AtProto/ -│ │ ├── AtProtoAuthSheet.tsx # login + session sheet -│ │ └── ImportSheet.tsx # Tangled import browser sheet -│ └── AppLayout/LayoutSettingsPanel/ -│ └── AtProtoSection.tsx -``` - -Auth UI is launched from the toolbar `@` button. When no session exists it opens the login sheet; when a session exists it opens the session indicator sheet. The login sheet also exposes a public "Browse Public Strings" path, and the session sheet exposes an "Import Strings" action that opens the pull browser. Logout is available from both the session sheet and the full settings panel. - -The pull browser flow is: - -1. Enter a handle or DID and call `string_list`. -2. Select a string and hydrate the preview with `string_get`. -3. Choose a Writer location + relative path. -4. Import with `doc_exists` guard + `doc_save`. - -Imported non-Markdown/non-plaintext strings are wrapped in fenced code blocks using the source filename extension as the language tag when possible. - -### Sync & Origin Tracking - -When a document is published as a string or imported from one, store the association in SQLite: - -- `string_origin` table: `doc_id`, `at_uri`, `tid`, `source_did`, `last_synced_at` -- On publish: insert/update origin row, store TID for future `putRecord` updates. -- On import: insert origin row linking the new document to the source string. -- Re-publish: detect local modifications to a previously published document; surface "Update String" action. -- Re-pull: compare `createdAt` or content hash to detect remote changes; prompt with diff. - -Non-markdown/non-plaintext content (detected by file extension on the string's `filename`) should be wrapped in a fenced code block with the appropriate language tag on import. - ---- - -## Standard.Site Integration - -- Phased: [Leaflet](https://tangled.org/leaflet.pub/leaflet/tree/main/lexicons/pub/leaflet) → pckt → GreenGale -- [ ] Pull posts from AT Protocol `standard.site` websites. -- [ ] Push posts to AT Protocol `standard.site` websites. -- See [standard.site](https://standard.site) & [repo](https://tangled.org/standard.site/lexicons) - ([Source](https://tangled.org/standard.site/lexicons/tree/main/src/lexicons)) - -### Pull Publications - -- `site.standard.publication` records (we can browser by users) - -## Pull Posts - -- `site.standard.document` records (posts) - - Leaflet content is `pub.leaflet.content` - - Pages: `pub.leaflet.pages.linearDocument` - -## Lexicon Reference (`sh.tangled.string`) - -```json -{ - "lexicon": 1, - "id": "sh.tangled.string", - "needsCbor": true, - "needsType": true, - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "required": ["filename", "description", "createdAt", "contents"], - "properties": { - "filename": { - "type": "string", - "maxGraphemes": 140, - "minGraphemes": 1 - }, - "description": { - "type": "string", - "maxGraphemes": 280 - }, - "createdAt": { - "type": "string", - "format": "datetime" - }, - "contents": { - "type": "string", - "minGraphemes": 1 - } - } - } - } - } -} -``` - -## AT Protocol OAuth - -### Desktop Loopback Flow - -1. User enters handle (e.g. `alice.bsky.social`). -2. Resolve handle → DID → PDS URL → authorization server metadata. -3. Generate ES256 DPoP keypair (one per session). -4. Pushed Authorization Request (PAR) with PKCE challenge (S256) to auth server. -5. Open system browser to `authorize?client_id=...&request_uri=...`. -6. User approves; browser redirects to `http://127.0.0.1:/callback?code=...`. -7. Exchange code for DPoP-bound access + refresh tokens. -8. Verify `sub` DID claim matches the resolved DID. -9. Use `Agent` for all subsequent XRPC calls. - -Jacquard's `jacquard-oauth` crate provides `OAuthClient`, `OAuthSession`, `FileAuthStore`, and `LoopbackConfig` to handle this flow. The `loopback` feature flag (enabled by default) includes the local HTTP server. - -### Token Lifecycle (Public/Native Client) - -- **Access token:** ~5 min expiry, refresh silently via refresh token. -- **Refresh token:** 2-week max lifetime. -- **Session:** 2-week max, then full re-auth required. -- **DPoP nonce:** 5-min server-side lifetime, rotated via `DPoP-Nonce` response header. Jacquard handles nonce rotation internally. - -### Session Persistence - -Use `FileAuthStore` (or equivalent) in the app data directory alongside the SQLite DB. This persists the DPoP keypair and refresh token across app restarts so users don't need to re-authenticate on every launch. On token expiry, attempt silent refresh; on failure, clear session and prompt re-login. - -### Client Metadata - -For development, `client_id` can be `http://localhost`. For production, publish client metadata at an HTTPS URL containing: - -- `application_type: "native"` -- `dpop_bound_access_tokens: true` -- `grant_types: ["authorization_code", "refresh_token"]` -- `scope: "atproto"` -- `redirect_uris: ["http://127.0.0.1/callback"]` - -## References - -- [Jacquard docs (v0.9.3)](https://docs.rs/jacquard/0.9.3/jacquard/) -- [Jacquard API: sh_tangled::string](https://docs.rs/jacquard-api/0.9.3/jacquard_api/sh_tangled/string/index.html) -- [Jacquard API: com_atproto::repo](https://docs.rs/jacquard-api/0.9.3/jacquard_api/com_atproto/repo/index.html) -- [AT Protocol: Repository spec](https://atproto.com/specs/repository) -- [AT Protocol: OAuth spec](https://atproto.com/specs/oauth) -- [AT Protocol: XRPC spec](https://atproto.com/specs/xrpc) -- [AT Protocol: Record keys](https://atproto.com/specs/record-key) -- [Tangled core repo](https://tangled.org/tangled.org/core) -- [Tangled string lexicon source](https://tangled.org/tangled.org/core/blob/master/lexicons/string/string.json) -- [Tangled blog: 6 months](https://blog.tangled.org/6-months) diff --git a/docs/parking-lot.md b/docs/parking-lot.md deleted file mode 100644 index 527cd25..0000000 --- a/docs/parking-lot.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: "Parking Lot" -description: > - A collection of ideas/proposals for new features and quick bug notes. -updated: 2026-03-19 ---- - -## Push/Pull - -See [AT Proto](./at-proto.md) - -### Post (Push) to - -- ATProto `standard.site` websites -- Tangled - -### Pull From - -- AtProto -- Tangled diff --git a/docs/roadmap.md b/docs/roadmap.md index 5a7bc76..5136c1a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,127 +3,4 @@ title: "Roadmap" last_updated: 2026-03-19 --- -## Tangled string integration - -Publish documents as [Tangled strings](https://tangled.sh) (AT Protocol gists) and import strings as documents. - -### Part 1 — Auth - -1. **OAuth loopback flow** - `src-tauri/src/atproto/auth.rs` -2. **Session persistence** - token + DPoP key storage in app data dir -3. **Tauri commands** - `atproto_login`, `atproto_logout`, `atproto_session_status` -4. **Frontend auth UI** - login sheet, session indicator, logout - - User clicks `@` button in toolbar - - If not logged in, show login sheet - - If logged in, show session indicator (Dolly from `icons.tsx`) - - Logout button in session indicator or in the settings menu - -### Part 2 — Pull - -1. **Tauri commands** - `string_list`, `string_get` -2. **Import UI** - "Import from Tangled" sheet with handle input, string browser, preview, import to location - - Fluent Icons (`i-fluent-document-*-16-filled`) - - Extensions covered: `py`, `md`, `js`, `ts`, `yaml`, `java`, `sass`, `css`, `csv`, `fs`, `cs` - - `i-fluent-document-16-filled` for fallback - -### Part 3 — Push - -1. **Tauri commands** - `string_create`, `string_update`, `string_delete` -2. **Publish UI** - "Publish as String" action in export menu with filename, description, preview - -### Part 4 — Sync & metadata - -1. **Origin tracking** - AT URI, TID, source DID in SQLite -2. **Change detection** - local re-publish offers, remote drift on re-pull - -## Content blocks (transclusion) - -Allow embedding external Markdown files, images, and CSV data into a master document using `/filename` syntax, as well as drag-and-drop of files into the editor. - -### Tasks - -1. **Syntax definition** - - `/path/to/file.md` on its own line = transclude that file's rendered content - - `/path/to/image.png` = embed image - - `/path/to/data.csv` = render as Markdown table - - Resolve paths relative to the current document's directory, scoped within its location -2. **Rust expansion command** - - `content_block_expand(doc_ref, block_ref) -> ExpandedBlock { kind, content }` - - Recursion guard: cap depth, detect cycles -3. **Editor integration** - - CodeMirror decoration: render content blocks inline as collapsed/expandable previews - - Syntax highlighting for the `/filename` token -4. **Preview + export** - - Expand content blocks during `markdown_render` for preview - - Expand during PDF/HTML export so final output is self-contained -5. **CSV → table rendering** - - Parse CSV, emit GFM table Markdown, feed into Comrak pipeline - -## Library enhancements (hashtags, smart folders, favorites) - -Improve file organization - -### Tasks - -1. **Hashtag extraction** - - Scan document text for `#tag` patterns (exclude Markdown headings) - - Store extracted tags in the SQLite index (`doc_tags` table) - - Re-index tags on save and on watcher events -2. **Task list extraction** - - Scan document for `- [ ]` patterns - - Store extracted tasks in the SQLite index (`doc_tasks` table) - - Re-index tasks on save and on watcher events -3. **Smart folders** - - Predefined filter rules: tag match, date range, word-count threshold, location - - `smart_folder_list() -> Vec`, `smart_folder_query(id) -> Vec` - - UI: render smart folders in the sidebar above/below locations -4. **Favorites** - - Toggle-favorite on any document (`doc_set_favorite(doc_ref, bool)`) - - Persist in SQLite; surface a "Favorites" virtual folder in the sidebar -5. **Sidebar UI updates** - - New sections for Smart Folders and Favorites - - Badge counts on each smart folder / favorites section - - Drag-and-drop reorder for smart folders - -## Hardening - -### Tasks - -1. **Outline utilization** - - Use Rust-generated `metadata.outline` from `markdown_render` in the UI for document structure navigation/jump-to-heading behavior -2. **Perf** - - Incremental render scheduling (debounce, worker thread) - - Indexing in background with progress events with UI feedback -3. **Recovery** - - Corrupt settings/workspace → app resets safely - - Missing location root → UI prompts to relink/remove - -## Standard.Site Pubs & Posts - -## GitHub Gist integration - -Import public gists, read personal/secret gists, and publish documents as gists. -Full spec in [docs/integration/gh.md](../integration/gh.md). - -### Part 1 — Public gist browsing - -1. **Backend gist module** — `src-tauri/src/github/{mod,gists}.rs` with `GithubState`, `GistRecord` -2. **Tauri commands** — `gist_list_public`, `gist_get` (no auth required) -3. **Frontend import UI** — `GistImportSheet.tsx` with username input, gist browser, preview, import to location -4. **Port + state wiring** — command wrappers in `ports/commands.ts`, `GithubUiState` in Zustand store, `useGithubUiState` selector - -### Part 2 — Auth & private gists - -1. **GitHub OAuth device flow** — `src-tauri/src/github/auth.rs` with `github_device_code`, `github_poll_token` -2. **Token persistence** — store access token in app data dir, restore on startup -3. **Tauri commands** — `github_session`, `github_logout`, `gist_list_personal` -4. **Auth UI** — `GithubAuthSheet.tsx` with device code display, session indicator, logout -5. **"My Gists" mode** — toggle in import sheet to browse personal + secret gists - -### Part 3 — Publish & update - -1. **Tauri commands** — `gist_create`, `gist_update`, `gist_delete` -2. **Publish UI** — `GistPublishSheet.tsx` with filename, description, visibility toggle, preview -3. **Origin tracking** — `gist_origin` SQLite table linking documents to gist IDs -4. **Re-publish** — detect previously published docs, surface "Update Gist" action -5. **Re-import** — compare `updated_at` to detect remote changes, prompt with diff +See files in `docs/tasks/` for other milestones diff --git a/docs/specs/at-proto.md b/docs/specs/at-proto.md new file mode 100644 index 0000000..0471534 --- /dev/null +++ b/docs/specs/at-proto.md @@ -0,0 +1,458 @@ +--- +title: AT Protocol Integration Spec +updated: 2026-03-19 +--- + +## Goals + +- Use [jacquard](https://docs.rs/crate/jacquard/latest) (`^0.9`) to handle AT Protocol interactions. +- AT Protocol OAuth with loopback redirect for desktop auth flow. +- Tangled string integration (publish and import documents as strings). +- Standard.Site post integration + +## Tangled Strings + +Strings are Tangled's equivalent of GitHub Gists — lightweight text/code snippets stored as AT Protocol records on the user's PDS under the `sh.tangled.string` collection. The Tangled AppView indexes them via Jetstream ingestion, but all CRUD goes through standard `com.atproto.repo.*` XRPC endpoints on the PDS directly — no Tangled-specific server API is needed for read/write. + +### Constraints + +- **Record size limit:** PDS records are capped at **2 MiB**. Large documents must be rejected with a clear error before attempting upload. +- **Filename:** 1–140 graphemes. Default to the document's current filename. +- **Description:** 0–280 graphemes. User-provided summary. +- **Contents:** min 1 grapheme. The document body (markdown or plaintext). +- **Key format:** TID (timestamp-based, base32-sortkey encoded, e.g. `3jzfcijpj2z2a`). +- **AT URI format:** `at:///sh.tangled.string/` + +### XRPC Endpoints + +All endpoints target `/xrpc/{NSID}` on the user's PDS. Collection is always `"sh.tangled.string"`. + +| Operation | Endpoint | Method | Auth | +| --------- | ------------------------------- | ------ | -------- | +| Create | `com.atproto.repo.createRecord` | POST | Required | +| Read | `com.atproto.repo.getRecord` | GET | No | +| List | `com.atproto.repo.listRecords` | GET | No | +| Update | `com.atproto.repo.putRecord` | POST | Required | +| Delete | `com.atproto.repo.deleteRecord` | POST | Required | +| Batch | `com.atproto.repo.applyWrites` | POST | Required | + +`listRecords` supports cursor-based pagination (`cursor`, `limit`, `reverse`, `rkeyStart`/`rkeyEnd`). Reading records does not require authentication — any user's public strings can be listed and fetched without a session. + +### Data Flow + +```text +┌──────────────┐ Tauri commands ┌───────────────────┐ +│ Frontend │ ───────────────────── │ src-tauri/ │ +│ (React/TS) │ │ commands.rs │ +│ │ ◄── CommandResponse │ + atproto.rs │ +└──────────────┘ └────────┬─────────┘ + │ + jacquard Agent + │ + ┌────────▼─────────┐ + │ User's PDS │ + │ (XRPC endpoints)│ + └──────────────────┘ + │ + Jetstream ingestion + │ + ┌────────▼────────────┐ + │ Tangled AppView │ + │ (tangled.sh) │ + └─────────────────────┘ +``` + +### Jacquard Usage + +Jacquard (`^0.9`, ~99k downloads/month, MPL-2.0) provides generated types for the `sh.tangled.string` lexicon in `jacquard_api::sh_tangled::string`: + +- `TangledString` / `TangledStringBuilder` — record type and builder. +- `TangledStringRecord` — full record with URI and CID. + +Sub-crates: + +| Crate | Purpose | +| ------------------- | --------------------------------------------------------------- | +| `jacquard-common` | Core types: DIDs, handles, AT URIs, NSIDs, TIDs, CIDs | +| `jacquard-api` | 646+ generated lexicon bindings (includes `sh_tangled::string`) | +| `jacquard-oauth` | OAuth/DPoP with loopback server support | +| `jacquard-identity` | Handle/DID resolution, OAuth metadata discovery | + +XRPC calls use the builder + `.send()` pattern: + +```rust +use jacquard::api::com_atproto::repo::create_record::CreateRecordRequest; +use jacquard::api::sh_tangled::string::TangledString; + +let record = TangledString::builder() + .filename("notes.md") + .description("My notes") + .contents(body) + .created_at(chrono::Utc::now()) + .build(); + +let response = CreateRecordRequest::builder() + .repo(&session.did) + .collection("sh.tangled.string") + .record(record) + .build() + .send(&agent) + .await?; +``` + +For reads (no auth needed), use a stateless `reqwest::Client` with the `XrpcExt` trait or an unauthenticated agent. For writes, the `Agent` handles DPoP headers and token refresh automatically. + +Jacquard types use zero-copy deserialization via `CowStr<'_>`. Use `.parse()` for borrowed data or `.into_output()` for owned `'static` data when the response outlives the buffer. + +### Backend Module Structure + +```sh +src-tauri/src/ +├── atproto/ +│ ├── mod.rs # re-exports shared auth types/state +│ ├── auth.rs # OAuth loopback flow, session restore, logout cleanup +│ └── strings.rs # Tangled string listing + fetch helpers +``` + +`AtProtoState` lives inside `AppState` and owns the Jacquard OAuth client plus persisted session metadata paths. The current auth slice restores an existing session during app startup, exposes the active `SessionInfo`, and clears persisted auth artifacts when restoration or logout fails. + +**Tauri commands:** + +| Command | Args | Returns | Auth | +| ------------------------ | -------------------------------------- | -------------------------------------- | --------- | +| `atproto_login` | `handle: String` | `CommandResponse` | Initiates | +| `atproto_logout` | — | `CommandResponse<()>` | Required | +| `atproto_session_status` | — | `CommandResponse>` | No | +| `string_create` | `filename, description, contents` | `CommandResponse` | Required | +| `string_update` | `tid, filename, description, contents` | `CommandResponse` | Required | +| `string_delete` | `tid` | `CommandResponse<()>` | Required | +| `string_list` | `did_or_handle` | `CommandResponse>` | No | +| `string_get` | `did_or_handle, tid` | `CommandResponse` | No | + +`StringRecord` contains: `uri` (AT URI), `tid`, `filename`, `description`, `contents`, `created_at`. + +### Frontend Structure + +```sh +src/ +├── state/stores/ui.ts # auth/import sheet mode + hydrated/pending/session state +├── state/selectors.ts # AT Protocol selector hooks +├── ports/commands.ts # atproto_login / logout / session_status / string_list / string_get +├── hooks/controllers/ +│ └── useAtProtoController.ts +├── components/ +│ ├── AtProto/ +│ │ ├── AtProtoAuthSheet.tsx # login + session sheet +│ │ └── ImportSheet.tsx # Tangled import browser sheet +│ └── AppLayout/LayoutSettingsPanel/ +│ └── AtProtoSection.tsx +``` + +Auth UI is launched from the toolbar `@` button. When no session exists it opens the login sheet; when a session exists it opens the session indicator sheet. The login sheet also exposes a public "Browse Public Strings" path, and the session sheet exposes an "Import Strings" action that opens the pull browser. Logout is available from both the session sheet and the full settings panel. + +The pull browser flow is: + +1. Enter a handle or DID and call `string_list`. +2. Select a string and hydrate the preview with `string_get`. +3. Choose a Writer location + relative path. +4. Import with `doc_exists` guard + `doc_save`. + +Imported non-Markdown/non-plaintext strings are wrapped in fenced code blocks using the source filename extension as the language tag when possible. + +### Sync & Origin Tracking + +When a document is published as a string or imported from one, store the association in SQLite: + +- `string_origin` table: `doc_id`, `at_uri`, `tid`, `source_did`, `last_synced_at` +- On publish: insert/update origin row, store TID for future `putRecord` updates. +- On import: insert origin row linking the new document to the source string. +- Re-publish: detect local modifications to a previously published document; surface "Update String" action. +- Re-pull: compare `createdAt` or content hash to detect remote changes; prompt with diff. + +Non-markdown/non-plaintext content (detected by file extension on the string's `filename`) should be wrapped in a fenced code block with the appropriate language tag on import. + +--- + +## Standard.Site Integration + +Pull and push long-form posts from/to AT Protocol publishing platforms that implement the [Standard.Site](https://standard.site) shared lexicons. The `site.standard.*` schemas are format-agnostic — each platform (Leaflet, pckt, GreenGale, etc.) fills the open `content` union with its own block model. Writer converts between these block formats and Markdown on import/export. + +Jacquard provides generated bindings for both Standard.Site and Leaflet lexicons behind feature flags: + +```toml +jacquard-api = { version = "0.9", features = ["pub_leaflet", "site_standard"] } +``` + +- `jacquard_api::site_standard::{publication, document, theme, graph}` — publication/document record types + builders +- `jacquard_api::pub_leaflet::{document, publication, blocks, pages, richtext, comment, graph}` — full Leaflet content model with block types, facets, and page layouts +- Phased platform support: [Leaflet](https://tangled.org/leaflet.pub/leaflet/tree/main/lexicons/pub/leaflet) → pckt → GreenGale +- See [standard.site](https://standard.site) & [repo](https://tangled.org/standard.site/lexicons) + ([Source](https://tangled.org/standard.site/lexicons/tree/main/src/lexicons)) + +### Data Model + +#### Publications (`site.standard.publication`) + +A publication is a named container (blog/site) owned by a DID. Key `tid`. + +| Field | Type | Required | Constraints | +| ------------- | --------------------------------- | -------- | ---------------------------------- | +| `url` | `string` (format: uri) | Yes | Base publication URL | +| `name` | `string` | Yes | maxLength 5000, maxGraphemes 500 | +| `description` | `string` | No | maxLength 30000, maxGraphemes 3000 | +| `icon` | `blob` (image/\*) | No | max 1 MiB, 256×256 min | +| `basicTheme` | ref → `site.standard.theme.basic` | No | | +| `preferences` | `#preferences` | No | `showInDiscover` (default true) | + +Verification: `/.well-known/site.standard.publication` on the publication URL returns the AT URI. + +#### Documents / Posts (`site.standard.document`) + +A document belongs to a publication and holds the post content. Key `tid`. + +| Field | Type | Required | Constraints | +| ------------- | ---------------------------------- | -------- | ------------------------------------- | +| `site` | `string` (AT URI or https URL) | Yes | Points to the publication | +| `title` | `string` | Yes | maxLength 5000, maxGraphemes 500 | +| `content` | `union([], closed: false)` | No | **Open union** — platform-specific | +| `textContent` | `string` | No | Plaintext fallback (no markup) | +| `path` | `string` | No | Slug appended to site URL | +| `description` | `string` | No | maxLength 30000, maxGraphemes 3000 | +| `coverImage` | `blob` (image/\*) | No | max 1 MiB | +| `tags` | `string[]` | No | maxLength 1280, maxGraphemes 128 each | +| `publishedAt` | `string` (format: datetime) | Yes | | +| `updatedAt` | `string` (format: datetime) | No | | +| `bskyPostRef` | ref → `com.atproto.repo.strongRef` | No | Cross-post reference | + +The `content` field is an **open union** (`closed: false`). Standard.Site deliberately does not prescribe a content format — each platform defines its own content type that fills this union. The `textContent` field provides a universal plaintext fallback for platforms that cannot render the native content blocks. + +#### Subscriptions (`site.standard.graph.subscription`) + +| Field | Type | Required | +| ------------- | ------------------------- | -------- | +| `publication` | `string` (format: at-uri) | Yes | + +### Leaflet Content Model + +Leaflet is the first target platform. Its documents use `pub.leaflet.pages.linearDocument` as the content type inside the Standard.Site `content` union. + +#### Document (`pub.leaflet.document`) + +| Field | Type | Required | +| ------------- | --------------------------------- | -------- | +| `title` | `string` | Yes | +| `author` | `string` (format: at-identifier) | Yes | +| `pages` | `union[linearDocument, canvas][]` | Yes | +| `publication` | `string` (format: at-uri) | No | +| `description` | `string` | No | +| `publishedAt` | `string` (format: datetime) | No | +| `tags` | `string[]` | No | +| `coverImage` | `blob` | No | + +#### Pages + +- **`pub.leaflet.pages.linearDocument`** — ordered array of blocks with optional alignment (`left` | `center` | `right` | `justify`). +- **`pub.leaflet.pages.canvas`** — blocks with spatial positioning (`x`, `y`, `width`, `height`, `rotation`). Canvas pages are not importable into Writer (skip with warning). + +#### Block Types + +| Block NSID | Required Fields | Markdown Equivalent | +| ----------------------------------- | -------------------------- | -------------------------------- | +| `pub.leaflet.blocks.text` | `plaintext` | Paragraph | +| `pub.leaflet.blocks.header` | `plaintext`, `level` (1-6) | `#`–`######` heading | +| `pub.leaflet.blocks.blockquote` | `plaintext` | `>` blockquote | +| `pub.leaflet.blocks.code` | `plaintext` | Fenced code block (+ `language`) | +| `pub.leaflet.blocks.image` | `image`, `aspectRatio` | `![alt](url)` | +| `pub.leaflet.blocks.orderedList` | `children` | `1.` list items (nested) | +| `pub.leaflet.blocks.unorderedList` | `children` | `-` list items (nested) | +| `pub.leaflet.blocks.horizontalRule` | — | `---` | +| `pub.leaflet.blocks.iframe` | — | Raw URL or omit | +| `pub.leaflet.blocks.math` | — | `$$` math block | +| `pub.leaflet.blocks.bskyPost` | — | Omit (not representable) | + +#### Rich Text Facets + +Leaflet uses Bluesky-style facets (`pub.leaflet.richtext.facet`) to annotate sub-strings of `plaintext` using byte-indexed slices (`byteStart`, `byteEnd`, UTF-8, start inclusive, end exclusive). + +| Facet Feature | Markdown Equivalent | +| ---------------- | -------------------------- | +| `#bold` | `**text**` | +| `#italic` | `*text*` | +| `#code` | `` `text` `` | +| `#strikethrough` | `~~text~~` | +| `#link` | `[text](url)` | +| `#underline` | No equivalent (skip) | +| `#highlight` | No equivalent (skip) | +| `#footnote` | `[^id]` / `[^id]: content` | +| `#didMention` | `[text](at://did)` | +| `#atMention` | `[@handle](at://did)` | + +### Conversion Strategy + +#### Leaflet → Markdown (Import) + +1. Fetch the document via `com.atproto.repo.getRecord` → deserialize into `pub_leaflet::document::Document`. +2. Match on `DocumentPagesItem` — process `LinearDocument` pages, skip `Canvas` with ``. +3. For each block (matched via Jacquard's block enums), convert to Markdown using the mapping above. +4. Apply `pub_leaflet::richtext::facet::Facet` annotations to `plaintext` fields — sort by `ByteSlice::byte_start`, walk the string, wrap annotated ranges in Markdown syntax. Match on `FacetFeaturesItem` variants (`Bold`, `Italic`, `Link`, etc.). Handle overlapping/nested facets by applying innermost first. +5. Join blocks with double newlines. +6. Unsupported blocks (`BskyPost`, `Iframe`, `Poll`, `Button`, `Website`, `Page`): emit ``. +7. Images: download blob from PDS via CID, save to the import location, reference with relative path. + +#### Markdown → Leaflet Blocks (Publish) + +1. Parse Markdown AST (via Comrak in Rust). +2. Map each AST node to the corresponding Jacquard block type using builders (e.g. `Header::builder().plaintext(text).level(2).facets(facets).build()`). +3. Convert inline formatting to `Facet` instances with `ByteSlice` indices on the plaintext representation. Construct via `FacetBuilder` + feature variant structs. +4. Upload images as blobs to the PDS, construct `pub_leaflet::blocks::image::Image` with the returned blob ref. +5. Wrap output blocks in a `LinearDocument` page, build the full `Document` via `DocumentBuilder`. + +### XRPC Endpoints & Jacquard Usage + +All Standard.Site/Leaflet CRUD uses the same `com.atproto.repo.*` endpoints as Tangled strings — no custom API is needed. Jacquard's generated types provide record structs, builders, and `GetRecordOutput` wrappers for both namespaces, following the same builder + `.send()` pattern used for strings. + +| Operation | Endpoint | Collection | Auth | +| ---------- | ------------------------------- | --------------------------- | -------- | +| List pubs | `com.atproto.repo.listRecords` | `site.standard.publication` | No | +| Get pub | `com.atproto.repo.getRecord` | `site.standard.publication` | No | +| List docs | `com.atproto.repo.listRecords` | `site.standard.document` | No | +| Get doc | `com.atproto.repo.getRecord` | `site.standard.document` | No | +| Create doc | `com.atproto.repo.createRecord` | `site.standard.document` | Required | +| Update doc | `com.atproto.repo.putRecord` | `site.standard.document` | Required | +| Delete doc | `com.atproto.repo.deleteRecord` | `site.standard.document` | Required | + +Key Jacquard types: + +- **Publications:** `site_standard::publication::Publication` / `PublicationBuilder` / `PublicationRecord` +- **Documents:** `site_standard::document::Document` / `DocumentBuilder` / `DocumentRecord` +- **Leaflet docs:** `pub_leaflet::document::Document` / `DocumentBuilder` — `DocumentPagesItem` enum for `linearDocument` vs `canvas` +- **Blocks:** `pub_leaflet::blocks::{text::Text, header::Header, blockquote::Blockquote, code::Code, image::Image, ...}` — each with builder +- **Facets:** `pub_leaflet::richtext::facet::{Facet, FacetBuilder, ByteSlice, FacetFeaturesItem}` — feature variants: `Bold`, `Italic`, `Code`, `Link`, `Strikethrough`, etc. + +For reads (no auth), use a stateless `reqwest::Client` with `XrpcExt` or an unauthenticated agent. For writes, the `Agent` handles DPoP + token refresh, same as with strings. Use `.into_output()` for owned `'static` data when records outlive the response buffer. + +### Backend Module Structure + +```sh +src-tauri/src/atproto/ +├── mod.rs # re-exports (existing) +├── auth.rs # OAuth (existing) +├── strings.rs # Tangled strings (existing) +├── standard_site.rs # publication + document listing/fetch via Jacquard site_standard types +└── leaflet.rs # Leaflet block ↔ Markdown conversion using Jacquard pub_leaflet types +``` + +Requires adding `pub_leaflet` and `site_standard` feature flags to the `jacquard-api` dependency in `src-tauri/Cargo.toml`. + +### Tauri Commands + +| Command | Args | Returns | Auth | +| ------------------- | ---------------------------------------------- | ----------------------------------------- | -------- | +| `publication_list` | `did_or_handle` | `CommandResponse>` | No | +| `publication_get` | `did_or_handle, tid` | `CommandResponse` | No | +| `post_list` | `did_or_handle, publication_tid?` | `CommandResponse>` | No | +| `post_get` | `did_or_handle, tid` | `CommandResponse` | No | +| `post_get_markdown` | `did_or_handle, tid` | `CommandResponse` | No | +| `post_create` | `publication_tid, title, markdown, tags?, ...` | `CommandResponse` | Required | +| `post_update` | `tid, title?, markdown?, tags?, ...` | `CommandResponse` | Required | +| `post_delete` | `tid` | `CommandResponse<()>` | Required | + +`PublicationRecord`: `uri`, `tid`, `name`, `description`, `url`. +`PostRecord`: `uri`, `tid`, `title`, `description`, `text_content`, `published_at`, `updated_at`, `tags`, `publication_uri`. + +`post_get_markdown` performs Leaflet→Markdown conversion server-side so the frontend receives ready-to-use content. + +### Frontend Structure + +```sh +src/ +├── ports/commands.ts # + publication_list, post_list, post_get, post_get_markdown, post_create, ... +├── state/stores/ui.ts # + StandardSiteUiState (import/publish sheet modes) +├── state/selectors.ts # + useStandardSiteUiState selector +├── hooks/controllers/ +│ └── useStandardSiteController.ts # publication browsing, post import/publish orchestration +├── components/ +│ └── StandardSite/ +│ ├── PostImportSheet.tsx # browse publications → posts → preview markdown → import +│ └── PostPublishSheet.tsx # select publication → fill title/tags → preview → publish +``` + +### Import Flow (Pull) + +1. User opens import sheet from toolbar or settings panel. +2. Enter a handle or DID → call `publication_list` to list their publications. +3. Select a publication → call `post_list` filtered by that publication. +4. Select a post → call `post_get_markdown` to preview the converted Markdown. +5. Choose a Writer location + relative path → import with `doc_exists` guard + `doc_save`. + +### Publish Flow (Push) + +1. User triggers "Publish to Standard.Site" from the export menu. +2. If user has publications, show publication picker. If none, prompt to create one (future). +3. Fill title, description, tags. Preview the Markdown that will be converted. +4. Call `post_create` → backend converts Markdown to Leaflet blocks, creates `site.standard.document` record. +5. Store origin tracking for future updates. + +### Origin Tracking + +Reuse the same pattern as Tangled strings: + +- `post_origin` table: `doc_id`, `at_uri`, `tid`, `publication_uri`, `source_did`, `last_synced_at` +- On publish: insert/update origin row. +- On import: insert origin row linking the new document to the source post. +- Re-publish: detect local modifications, surface "Update Post" action. +- Re-pull: compare `updatedAt` / content hash to detect remote changes. + +## AT Protocol OAuth + +### Desktop Loopback Flow + +1. User enters handle (e.g. `alice.bsky.social`). +2. Resolve handle → DID → PDS URL → authorization server metadata. +3. Generate ES256 DPoP keypair (one per session). +4. Pushed Authorization Request (PAR) with PKCE challenge (S256) to auth server. +5. Open system browser to `authorize?client_id=...&request_uri=...`. +6. User approves; browser redirects to `http://127.0.0.1:/callback?code=...`. +7. Exchange code for DPoP-bound access + refresh tokens. +8. Verify `sub` DID claim matches the resolved DID. +9. Use `Agent` for all subsequent XRPC calls. + +Jacquard's `jacquard-oauth` crate provides `OAuthClient`, `OAuthSession`, `FileAuthStore`, and `LoopbackConfig` to handle this flow. The `loopback` feature flag (enabled by default) includes the local HTTP server. + +### Token Lifecycle (Public/Native Client) + +- **Access token:** ~5 min expiry, refresh silently via refresh token. +- **Refresh token:** 2-week max lifetime. +- **Session:** 2-week max, then full re-auth required. +- **DPoP nonce:** 5-min server-side lifetime, rotated via `DPoP-Nonce` response header. Jacquard handles nonce rotation internally. + +### Session Persistence + +Use `FileAuthStore` (or equivalent) in the app data directory alongside the SQLite DB. This persists the DPoP keypair and refresh token across app restarts so users don't need to re-authenticate on every launch. On token expiry, attempt silent refresh; on failure, clear session and prompt re-login. + +### Client Metadata + +For development, `client_id` can be `http://localhost`. For production, publish client metadata at an HTTPS URL containing: + +- `application_type: "native"` +- `dpop_bound_access_tokens: true` +- `grant_types: ["authorization_code", "refresh_token"]` +- `scope: "atproto"` +- `redirect_uris: ["http://127.0.0.1/callback"]` + +## References + +- [Jacquard docs (v0.9.3)](https://docs.rs/jacquard/0.9.3/jacquard/) +- [Jacquard API: sh_tangled::string](https://docs.rs/jacquard-api/0.9.3/jacquard_api/sh_tangled/string/index.html) +- [Jacquard API: com_atproto::repo](https://docs.rs/jacquard-api/0.9.3/jacquard_api/com_atproto/repo/index.html) +- [AT Protocol: Repository spec](https://atproto.com/specs/repository) +- [AT Protocol: OAuth spec](https://atproto.com/specs/oauth) +- [AT Protocol: XRPC spec](https://atproto.com/specs/xrpc) +- [AT Protocol: Record keys](https://atproto.com/specs/record-key) +- [Tangled core repo](https://tangled.org/tangled.org/core) +- [Tangled string lexicon source](https://tangled.org/tangled.org/core/blob/master/lexicons/string/string.json) +- [Tangled blog: 6 months](https://blog.tangled.org/6-months) +- [Standard.Site](https://standard.site) +- [Standard.Site lexicon repo](https://tangled.org/standard.site/lexicons) +- [Standard.Site lexicon source](https://tangled.org/standard.site/lexicons/tree/main/src/lexicons) +- [Leaflet lexicon source](https://tangled.org/leaflet.pub/leaflet/tree/main/lexicons/pub/leaflet) diff --git a/docs/integration/gh.md b/docs/specs/integration/gh.md similarity index 100% rename from docs/integration/gh.md rename to docs/specs/integration/gh.md diff --git a/docs/tasks/gh.md b/docs/tasks/gh.md new file mode 100644 index 0000000..9b5f295 --- /dev/null +++ b/docs/tasks/gh.md @@ -0,0 +1,30 @@ +--- +title: "GitHub Gist Integration" +last_updated: 2026-03-20 +--- + +Import public gists, read personal/secret gists, and publish documents as gists. +Full spec in [docs/integration/gh.md](../integration/gh.md). + +## Part 1 — Public gist browsing + +1. **Backend gist module** — `src-tauri/src/github/{mod,gists}.rs` with `GithubState`, `GistRecord` +2. **Tauri commands** — `gist_list_public`, `gist_get` (no auth required) +3. **Frontend import UI** — `GistImportSheet.tsx` with username input, gist browser, preview, import to location +4. **Port + state wiring** — command wrappers in `ports/commands.ts`, `GithubUiState` in Zustand store, `useGithubUiState` selector + +## Part 2 — Auth & private gists + +1. **GitHub OAuth device flow** — `src-tauri/src/github/auth.rs` with `github_device_code`, `github_poll_token` +2. **Token persistence** — store access token in app data dir, restore on startup +3. **Tauri commands** — `github_session`, `github_logout`, `gist_list_personal` +4. **Auth UI** — `GithubAuthSheet.tsx` with device code display, session indicator, logout +5. **"My Gists" mode** — toggle in import sheet to browse personal + secret gists + +## Part 3 — Publish & update + +1. **Tauri commands** — `gist_create`, `gist_update`, `gist_delete` +2. **Publish UI** — `GistPublishSheet.tsx` with filename, description, visibility toggle, preview +3. **Origin tracking** — `gist_origin` SQLite table linking documents to gist IDs +4. **Re-publish** — detect previously published docs, surface "Update Gist" action +5. **Re-import** — compare `updated_at` to detect remote changes, prompt with diff diff --git a/docs/tasks/lib.md b/docs/tasks/lib.md new file mode 100644 index 0000000..c35e73c --- /dev/null +++ b/docs/tasks/lib.md @@ -0,0 +1,28 @@ +--- +title: Library Enhancements +updated: 2026-03-20 +--- + +Improve file organization + +## Tasks + +1. **Hashtag extraction** + - Scan document text for `#tag` patterns (exclude Markdown headings) + - Store extracted tags in the SQLite index (`doc_tags` table) + - Re-index tags on save and on watcher events +2. **Task list extraction** + - Scan document for `- [ ]` patterns + - Store extracted tasks in the SQLite index (`doc_tasks` table) + - Re-index tasks on save and on watcher events +3. **Smart folders** + - Predefined filter rules: tag match, date range, word-count threshold, location + - `smart_folder_list() -> Vec`, `smart_folder_query(id) -> Vec` + - UI: render smart folders in the sidebar above/below locations +4. **Favorites** + - Toggle-favorite on any document (`doc_set_favorite(doc_ref, bool)`) + - Persist in SQLite; surface a "Favorites" virtual folder in the sidebar +5. **Sidebar UI updates** + - New sections for Smart Folders and Favorites + - Badge counts on each smart folder / favorites section + - Drag-and-drop reorder for smart folders diff --git a/docs/tasks/parking-lot.md b/docs/tasks/parking-lot.md new file mode 100644 index 0000000..c7a71da --- /dev/null +++ b/docs/tasks/parking-lot.md @@ -0,0 +1,15 @@ +--- +title: "Parking Lot" +description: > + A collection of ideas/proposals for new features and quick bug notes. +updated: 2026-03-19 +--- + +1. **Outline utilization** + - Use Rust-generated `metadata.outline` from `markdown_render` in the UI for document structure navigation/jump-to-heading behavior +2. **Perf** + - Incremental render scheduling (debounce, worker thread) + - Indexing in background with progress events with UI feedback +3. **Recovery** + - Corrupt settings/workspace → app resets safely + - Missing location root → UI prompts to relink/remove diff --git a/docs/tasks/standard-site.md b/docs/tasks/standard-site.md new file mode 100644 index 0000000..2084cd8 --- /dev/null +++ b/docs/tasks/standard-site.md @@ -0,0 +1,51 @@ +--- +title: "Standard.Site Pubs & Posts integration" +updated: 2026-03-20 +--- + +Pull and push long-form posts from/to AT Protocol publishing platforms using Standard.Site shared lexicons. Leaflet is the first target platform. All record types and builders come from Jacquard's `pub_leaflet` and `site_standard` feature flags. + +## Part 1 — Leaflet Block ↔ Markdown Conversion + +1. **Leaflet → Markdown converter** — `src-tauri/src/atproto/leaflet.rs` + - Deserialize `pub_leaflet::document::Document`, match on `DocumentPagesItem` variants + - Map Jacquard block types (`blocks::text::Text`, `blocks::header::Header`, etc.) to Markdown equivalents + - Convert `pub_leaflet::richtext::facet::Facet` annotations (matching on `FacetFeaturesItem` variants: `Bold`, `Italic`, `Link`, `Code`, `Strikethrough`, etc.) to inline Markdown syntax + - Handle nested list items with recursive conversion + - Skip `Canvas` pages and unsupported block variants with comment markers +2. **Markdown → Leaflet block builder** — same module, reverse direction + - Parse Markdown AST via Comrak + - Construct Jacquard block types using builders (e.g. `Header::builder().plaintext(text).level(2).facets(facets).build()`) + - Convert inline formatting to `Facet` instances with `ByteSlice` indices via `FacetBuilder` + - Wrap output in a `LinearDocument` page, build full `Document` via `DocumentBuilder` +3. **Rust tests** — round-trip conversion tests for each block type and facet combination + +## Part 2 — Pull (Import Posts) + +1. **Backend helpers** — `src-tauri/src/atproto/standard_site.rs` + - `listRecords` wrapper for `site_standard::publication::Publication` and `site_standard::document::Document` + - `getRecord` wrapper deserializing into Jacquard types, extracting Leaflet content from the `content` open union +2. **Tauri commands** — `publication_list`, `publication_get`, `post_list`, `post_get`, `post_get_markdown` +3. **Frontend import UI** — `PostImportSheet.tsx` + - Enter handle/DID → browse publications → browse posts → preview converted Markdown → import to location + - Reuse existing import patterns from `ImportSheet.tsx` +4. **Port + state wiring** — command wrappers in `ports/commands.ts`, `StandardSiteUiState` in Zustand store, `useStandardSiteUiState` selector +5. **Image handling** — download blobs from PDS via CID, save to import location, reference with relative paths + +## Part 3 — Push (Publish Posts) + +1. **Tauri commands** — `post_create`, `post_update`, `post_delete` + - Accept Markdown + metadata, convert to Leaflet blocks via Jacquard builders server-side + - Upload images as blobs to PDS, construct `pub_leaflet::blocks::image::Image` with returned blob ref + - Build `site_standard::document::Document` via `DocumentBuilder` with Leaflet content in the `content` union +2. **Publish UI** — `PostPublishSheet.tsx` + - Publication picker (list user's publications via `site_standard::publication` records) + - Title, description, tags input with preview + - Integrated into export menu alongside "Publish as String" +3. **Origin tracking** — `post_origin` SQLite table (`doc_id`, `at_uri`, `tid`, `publication_uri`, `source_did`, `last_synced_at`) + +## Part 4 — Sync & Re-publish + +1. **Re-publish detection** — detect local modifications to previously published posts, surface "Update Post" action +2. **Re-pull detection** — compare `updatedAt` / content hash to detect remote changes, prompt with diff +3. **Publication management** — create new publications from Writer (future, dependent on Standard.Site ecosystem maturity) diff --git a/docs/tasks/syntax-extensions.md b/docs/tasks/syntax-extensions.md new file mode 100644 index 0000000..30419fd --- /dev/null +++ b/docs/tasks/syntax-extensions.md @@ -0,0 +1,25 @@ +--- +title: Syntax Extensions +updated: 2026-03-20 +--- + +Allow embedding external Markdown files, images, and CSV data into a master document using `/filename` syntax, as well as drag-and-drop of files into the editor. + +## Tasks + +1. **Syntax definition** + - `/path/to/file.md` on its own line = transclude that file's rendered content + - `/path/to/image.png` = embed image + - `/path/to/data.csv` = render as Markdown table + - Resolve paths relative to the current document's directory, scoped within its location +2. **Rust expansion command** + - `content_block_expand(doc_ref, block_ref) -> ExpandedBlock { kind, content }` + - Recursion guard: cap depth, detect cycles +3. **Editor integration** + - CodeMirror decoration: render content blocks inline as collapsed/expandable previews + - Syntax highlighting for the `/filename` token +4. **Preview + export** + - Expand content blocks during `markdown_render` for preview + - Expand during PDF/HTML export so final output is self-contained +5. **CSV → table rendering** + - Parse CSV, emit GFM table Markdown, feed into Comrak pipeline diff --git a/docs/tasks/tangled.md b/docs/tasks/tangled.md new file mode 100644 index 0000000..e62ac09 --- /dev/null +++ b/docs/tasks/tangled.md @@ -0,0 +1,35 @@ +--- +title: Tangled Strings integration +updated: 2026-03-20 +--- + +Publish documents as [Tangled strings](https://tangled.sh) (AT Protocol gists) and import strings as documents. + +## Part 1 — Auth + +1. **OAuth loopback flow** - `src-tauri/src/atproto/auth.rs` +2. **Session persistence** - token + DPoP key storage in app data dir +3. **Tauri commands** - `atproto_login`, `atproto_logout`, `atproto_session_status` +4. **Frontend auth UI** - login sheet, session indicator, logout + - User clicks `@` button in toolbar + - If not logged in, show login sheet + - If logged in, show session indicator (Dolly from `icons.tsx`) + - Logout button in session indicator or in the settings menu + +## Part 2 — Pull + +1. **Tauri commands** - `string_list`, `string_get` +2. **Import UI** - "Import from Tangled" sheet with handle input, string browser, preview, import to location + - Fluent Icons (`i-fluent-document-*-16-filled`) + - Extensions covered: `py`, `md`, `js`, `ts`, `yaml`, `java`, `sass`, `css`, `csv`, `fs`, `cs` + - `i-fluent-document-16-filled` for fallback + +## Part 3 — Push + +1. **Tauri commands** - `string_create`, `string_update`, `string_delete` +2. **Publish UI** - "Publish as String" action in export menu with filename, description, preview + +## Part 4 — Sync & metadata + +1. **Origin tracking** - AT URI, TID, source DID in SQLite +2. **Change detection** - local re-publish offers, remote drift on re-pull