feat: add shared decks and multi-account Mastodon OAuth
This commit is contained in:
+94
-104
@@ -1,140 +1,130 @@
|
||||
# Research decks
|
||||
|
||||
## Workspace interface
|
||||
## Workspace
|
||||
|
||||
The deck occupies the viewport with a dark sidebar and horizontally arranged,
|
||||
independently scrolling columns. A compact toolbar names the active deck.
|
||||
The sidebar switches decks, adds columns, and jumps to a column; on mobile,
|
||||
it becomes a compact top bar. Column header menus expose editing, ordering,
|
||||
and deletion. Creation and editing use native modal dialogs with Escape and
|
||||
focus restoration. Tokens use a navy/blue palette and a system sans font.
|
||||
This replaces the original spacious Garden reader layout. Layout inspiration:
|
||||
[Twitter's TweetDeck design notes](https://blog.x.com/en_us/a/2012/designing-the-new-tweetdeck).
|
||||
Both `/` and `/deck` render the deck-only application. A dark sidebar switches
|
||||
decks, adds columns, and jumps to a column. Up to six ordered columns scroll
|
||||
independently; only the active deck mounts and fetches its columns. On mobile,
|
||||
the sidebar becomes a compact top bar. Native dialogs handle creation and
|
||||
editing, with Escape and focus restoration. Column menus offer editing,
|
||||
ordering, and deletion; refresh and pagination are manual.
|
||||
|
||||
## Product direction
|
||||
Twitter and Mastodon can appear together, with different accounts in each
|
||||
column. Original-post links open their source site. Separate reader, search,
|
||||
list, user-profile, and conversation pages are not provided.
|
||||
|
||||
A research topic becomes a TweetDeck-style workspace: columns represent
|
||||
questions or perspectives and will eventually collect posts across Twitter,
|
||||
Mastodon, Bluesky, Threads, and Nostr for AI summaries with source references.
|
||||
The current implementation supports Twitter only, with manually or
|
||||
WebMCP-authored decks. Built-in planning, summaries, and other connectors
|
||||
remain future work.
|
||||
## Definitions and connections
|
||||
|
||||
## Workspace and column model
|
||||
`src/features/decks/model.ts` validates deck IDs, titles, ordered columns, and
|
||||
unique column IDs within each deck. Each column contains `id`, `title`,
|
||||
`connectionId`, and a platform-specific `source`.
|
||||
|
||||
Both `/` and `/deck` render the same deck-only application. Separate reader,
|
||||
search, list, user-profile, and conversation pages have been removed.
|
||||
Original-post links open X.
|
||||
| Platform | Source kind | Conditions |
|
||||
| --- | --- | --- |
|
||||
| Twitter | `search` | Native `query`, `product` (`Top`/`Latest`), `following` |
|
||||
| Twitter | `user` | `target`: handle or X/Twitter profile URL |
|
||||
| Twitter | `list` | `target`: numeric list ID or X/Twitter list URL |
|
||||
| Mastodon | `search` | `query`; results depend on the instance's search configuration |
|
||||
| Mastodon | `user` | `target`: account handle or the connected instance's numeric account ID |
|
||||
| Mastodon | `list` | `target`: numeric list ID for the connected account |
|
||||
| Mastodon | `hashtag` | `target`: tag without `#`, containing letters, numbers, or underscores |
|
||||
|
||||
`src/features/decks/model.ts` validates a version-2 workspace containing
|
||||
`activeDeckId` and one or more named decks. Each deck has a stable ID and up to
|
||||
six ordered columns. Only the active deck mounts its columns. The UI supports
|
||||
creating, selecting, renaming, and deleting deck profiles; the last deck cannot
|
||||
be deleted. Deleting the active deck selects the first remaining deck.
|
||||
`connectionId` is the account binding, distinct from a deck's name. Twitter
|
||||
connections resolve to profiles discovered from the configured relay.
|
||||
Mastodon connections identify accounts by instance origin and remote account
|
||||
ID; OAuth tokens stay encrypted on the server. The connection manager supports
|
||||
adding, reconnecting, and disconnecting Mastodon accounts. See
|
||||
[storage and OAuth](storage-and-oauth.md) for configuration and recovery.
|
||||
|
||||
Each column has a stable ID, title, required `profileName`, and one source:
|
||||
The editor discovers lists for the selected account. Changing accounts clears
|
||||
target-based conditions so an instance-local ID is not reused accidentally.
|
||||
Searches can retain conditions between accounts on the same platform. There is
|
||||
no global account selector or fallback to another account. Missing or
|
||||
disconnected bindings produce errors.
|
||||
|
||||
| Source kind | Conditions |
|
||||
| --- | --- |
|
||||
| `search` | Native Twitter `query`, `product` (`Top`/`Latest`), and `following` |
|
||||
| `user` | `target`: handle or X/Twitter profile URL, normalized to a handle |
|
||||
| `list` | `target`: numeric ID or X/Twitter list URL, normalized to an ID |
|
||||
The complete source and connection ID form query-cache identity. Equal
|
||||
conditions on the same connection share pages; different connections retain
|
||||
separate results and cursors. Deck synchronization does not poll post feeds.
|
||||
|
||||
All sources currently require `platform: "twitter"`. Unsupported definitions
|
||||
fail validation. Column IDs must be unique within a deck, and deck IDs within
|
||||
the workspace. Manual edits and agent tools use the same final schema.
|
||||
## Implementation boundaries
|
||||
|
||||
`profileName` is the relay account binding, distinct from a named deck profile.
|
||||
The editor discovers names through `/profiles` and lists through the selected
|
||||
profile. A column's profile can be changed independently. Every feed and list
|
||||
request carries an explicit profile name; the server confirms it still exists.
|
||||
Deleted profiles and unavailable discovery produce errors instead of falling
|
||||
back to another account. There is no browser-wide profile selection.
|
||||
- `column-editor.tsx` owns the title, connection binding, and final submission.
|
||||
- `column-source-editor.tsx` dispatches by platform and defines rebinding rules.
|
||||
- `twitter-source-editor.tsx` and `mastodon-source-editor.tsx` own each
|
||||
platform's source selection, fields, and account-specific list discovery.
|
||||
- `use-research-feed.ts` dispatches requests and keys the cache by connection
|
||||
and source. Server functions resolve credentials and fetch upstream pages.
|
||||
- `platforms/types.ts` defines normalized `ResearchPost` and `ResearchPage`
|
||||
records consumed by cards and column tools, without raw provider responses
|
||||
or credentials.
|
||||
|
||||
The complete source and profile participate in query-cache identity. Equal
|
||||
conditions on the same profile share loaded pages; distinct profiles retain
|
||||
separate results and cursors. Columns have independent refresh, pagination,
|
||||
and error state. Pagination and refresh are manual, with no polling.
|
||||
Twitter posts use `twitter:<id>` keys. Mastodon posts use canonical status URIs
|
||||
for keys and retain the fetched instance's native status ID separately. Boosts
|
||||
keep the wrapper identity and identify the boosting account. Mastodon HTML is
|
||||
sanitized on the server; cards honor content warnings and sensitive media.
|
||||
|
||||
## Platform boundary
|
||||
## Saved decks and temporary views
|
||||
|
||||
`src/features/platforms/types.ts` defines the display/evidence record without
|
||||
Bird imports: stable key, platform, native identity, original URL, text,
|
||||
author, optional publication time, media, and quoted post. The Twitter mapper
|
||||
uses `twitter:<id>` keys rather than mutable author handles. Raw responses
|
||||
and credentials do not enter this record. Cards consume the normalized record;
|
||||
engagement metrics and Twitter article previews are not normalized yet.
|
||||
SQLite is authoritative for saved definitions. Creating a named deck through
|
||||
the UI saves it; subsequent saved-deck edits and deletions use a revision check
|
||||
in a transaction. A stale revision is rejected. Save failures remain visible
|
||||
and do not report unsaved edits as persisted.
|
||||
|
||||
The source union is the extension point for future connectors. Twitter search
|
||||
syntax and ranking controls are provider-specific. When implementing another
|
||||
connector, add its real schema and server operation, normalize stable identity,
|
||||
and bind connection details into cache identity. Multiple sources in one
|
||||
column should wait until a second connector exercises that need; each source
|
||||
must retain its own opaque continuation and error state.
|
||||
Saved decks refresh on focus and every five seconds while visible. Refresh is
|
||||
held while an editor is open. Active selection is a local preference under
|
||||
`twitter-lite-active-deck`; selecting a deck does not switch another device's
|
||||
view. Deleting the active deck selects a remaining one. When none remain, the
|
||||
app opens an empty temporary view.
|
||||
|
||||
## Persistence
|
||||
WebMCP-created views are temporary by default. Temporary copies of saved decks
|
||||
also remain in this tab's memory. Editing them does not write to SQLite. The
|
||||
explicit save action replaces a temporary view with a shared saved deck,
|
||||
preserving its ID for idempotent retries. Failed saves retain temporary
|
||||
content. Temporary views disappear on reload or navigation, including OAuth
|
||||
redirects. Only definitions are saved: posts, cursors, and scroll positions
|
||||
are not archived.
|
||||
|
||||
The workspace is stored under `twitter-lite-research-deck` in localStorage,
|
||||
including all deck definitions and the active selection. It stores conditions
|
||||
and relay profile names, not credentials, posts, summaries, or cursors.
|
||||
Reloading fetches first pages of the selected deck. SSR and the first browser
|
||||
render show a loading state until storage has been read.
|
||||
The UI offers an explicit, one-time import of valid version-2 data from
|
||||
`twitter-lite-research-deck` in localStorage. It resolves old relay profile
|
||||
names to connection IDs and creates new saved deck IDs transactionally. A
|
||||
server marker prevents repeated imports; different content after the first
|
||||
import is rejected. The browser copy is removed only after success. Invalid
|
||||
or unsupported data is left untouched. HTTP and HTTPS have separate browser
|
||||
storage, but authorized devices read the same server-backed decks.
|
||||
|
||||
There is no automatic migration of the former single-deck format, which did
|
||||
not pin profiles to columns. Invalid or older saved data remains untouched
|
||||
while the UI presents an empty workspace and an error. An explicit saved edit
|
||||
replaces it. Storage failures are visible: changes still apply in the current
|
||||
tab, but persistence failures mean they will be lost on reload. Tabs and
|
||||
devices do not synchronize; the last write to an origin's localStorage wins.
|
||||
HTTP and HTTPS origins maintain separate workspaces.
|
||||
## WebMCP
|
||||
|
||||
## Deck WebMCP tools
|
||||
|
||||
Both deck routes expose workspace management and active-column reading.
|
||||
`list_decks` discovers definitions and available relay profiles; `get_deck`
|
||||
reads a specific or active deck. `set_deck` creates or replaces and activates a
|
||||
deck; `select_deck` and `delete_deck` operate by ID. `get_column_posts` and
|
||||
`load_more_column` read or paginate columns in the active deck. See the
|
||||
[full tool contracts](webmcp-prototype.md).
|
||||
|
||||
For example, after discovering a relay profile named `main`, create a deck:
|
||||
Use `list_connections` to discover bindings, then `set_deck` to create a
|
||||
temporary view. This example uses IDs returned by discovery:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "WebMCPの反応",
|
||||
"columns": [
|
||||
{
|
||||
"title": "日本語",
|
||||
"profileName": "main",
|
||||
"source": { "kind": "search", "query": "WebMCP lang:ja" }
|
||||
"title": "Twitter",
|
||||
"connectionId": "twitter-connection-id",
|
||||
"source": { "platform": "twitter", "kind": "search", "query": "WebMCP lang:ja" }
|
||||
},
|
||||
{
|
||||
"title": "開発者",
|
||||
"profileName": "main",
|
||||
"source": { "kind": "user", "target": "@example" }
|
||||
"title": "Mastodon",
|
||||
"connectionId": "mastodon-connection-id",
|
||||
"source": { "platform": "mastodon", "kind": "hashtag", "target": "WebMCP" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Omitting `deckId` creates a deck. To edit, read first and include its `deckId`
|
||||
and every column to retain; keep existing column IDs. Omitted columns are
|
||||
removed and omitted column IDs are generated. Post loading is asynchronous,
|
||||
so a successful save does not mean the upstream requests succeeded.
|
||||
|
||||
## Future AI work
|
||||
|
||||
A planner can generate definitions through the existing schema and tools.
|
||||
Summaries will need persisted collection snapshots: post identity and URL,
|
||||
retrieval time, source conditions, and profile context. Saved conditions alone
|
||||
do not preserve the evidence behind a summary. ACP or Codex app-server may
|
||||
connect a future planner, but neither is part of this implementation.
|
||||
Read the returned deck ID and call `save_deck` only when the view should be
|
||||
shared. Replacing a saved deck requires its `deckId` and `expectedRevision`;
|
||||
include every column to retain. Post loading is asynchronous and can fail
|
||||
independently of saving. See [all tool contracts](webmcp-prototype.md).
|
||||
|
||||
## Verification
|
||||
|
||||
Unit tests cover normalization, source/workspace validation, ordering,
|
||||
profile-specific caching and pagination, profile discovery failures, and
|
||||
storage behavior. Playwright uses a standalone mock relay through real server
|
||||
functions to exercise deck switching, column/profile editing, list selection,
|
||||
pagination, persistence, and native WebMCP. Accessibility checks run on the
|
||||
workspace. Automated tests do not need a live SNS search.
|
||||
Unit tests exercise source normalization, account-specific caches, OAuth,
|
||||
credential storage, revisions, import, and temporary-view behavior. Playwright
|
||||
uses an isolated SQLite database and mock relay through real server functions
|
||||
to exercise shared decks across browser contexts, editing, persistence,
|
||||
pagination, and native WebMCP. Deterministic automated tests do not require
|
||||
live SNS credentials.
|
||||
|
||||
Reference in New Issue
Block a user