feat: add research decks with WebMCP controls

This commit is contained in:
2026-09-24 15:38:06 +09:00
parent 2bc052c354
commit da8f52e605
32 changed files with 1876 additions and 51 deletions
+134
View File
@@ -0,0 +1,134 @@
# Research decks
## Product direction
A research topic should become a TweetDeck-style workspace: columns represent
questions or perspectives, collect posts across social platforms, and support
AI summaries with references back to the evidence. The intended platforms are
Twitter, Mastodon, Bluesky, Threads, and Nostr.
The first increment is a manually composed deck backed by Twitter. It makes
the deck definition, column lifecycle, and post normalization concrete before
adding AI or more connectors.
## Implemented boundary
- `src/features/decks/model.ts` validates a versioned deck definition: name,
ordered columns with stable IDs, and provider-specific search conditions.
At most six columns are loaded, bounding concurrent first-page requests.
- `src/features/platforms/types.ts` defines the display/evidence record without
importing Bird: stable key, platform, native identity, original URL, text,
author, optional publication time, media, and quoted post.
- `src/features/platforms/twitter.ts` maps existing reader posts into that
record. Keys use `twitter:<id>` rather than the author's mutable handle.
Raw provider responses and credentials do not enter the common record.
- `ResearchColumn` uses the existing Twitter search hook and server functions.
Equal search conditions share the reader's query cache. Distinct conditions
have independent loading/error/pagination state. Profile changes invalidate
queries through the existing profile switcher.
- `ResearchPostCard` only consumes the common record. Provider-specific detail
routes remain in the existing Twitter reader. Original-post links, media,
and one level of quotes are shown in the deck; engagement metrics and
Twitter article previews are not normalized yet.
The current source is a single Twitter search per column. Its `platform`
discriminator is the extension point for another real connector, not a claim
that five adapters already work. Twitter's raw syntax, Top/Latest, and follows
filter are not requirements imposed on the other platforms. Unsupported
source definitions fail validation instead of silently dropping conditions.
The existing reader and its active-feed WebMCP tools remain separate from the
deck. Mounting multiple reader `PostFeed` components would register conflicting
active-feed tools; deck columns therefore use the search hook directly.
On `/deck`, `get_deck` and `set_deck` expose the definition once local storage
has loaded. They unregister when leaving the route.
## Deck WebMCP tools
`get_deck({})` returns the current definition (including column IDs) and any
storage error. `set_deck` replaces the complete ordered definition and closes
unsaved editor forms. Read before editing, keep IDs of retained columns, and
include every column you want to keep. Omit IDs for new columns. An empty array
clears the deck. The same deck schema validates the entire input before changes.
```json
{
"title": "WebMCPの反応",
"columns": [
{ "title": "日本語", "source": { "query": "WebMCP lang:ja" } },
{ "title": "海外の話題", "source": { "query": "WebMCP lang:en", "product": "Top" } }
]
}
```
`source.platform` defaults to `twitter`, `product` to `Latest`, and `following`
to false. `set_deck` returns the applied definition and `persisted: true` before
post loading completes. It does not claim that searches succeeded. A storage
failure returns `isError: true` and explains that the in-memory change was
applied but will be lost on reload. Invalid input changes neither UI nor storage.
Native WebMCP needs a supported browser and a secure context. Use the HTTPS
Tailscale Serve origin rather than an HTTP tailnet IP. For Vite, allow that exact
hostname through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the dev process's
environment. HTTP and HTTPS origins have separate browser-local deck storage.
## Persistence and fetching
One deck is saved under `twitter-lite-research-deck` in localStorage. It contains
only conditions and names, not posts, summaries, credentials, or cursors.
SSR and the initial browser render show a loading state before reading it.
Invalid saved data is retained until the user explicitly saves an edit. Storage
read/write failures are surfaced in the UI.
The selected relay profile is browser-wide, not pinned to a column. Reopening
a deck uses that current profile and fetches first pages. Pagination is manual;
there is no polling or background collection. Tabs do not synchronize deck
edits; the last edit written to localStorage wins. LocalStorage is an initial
single-browser workspace, not the eventual research archive.
## Adding the second platform
1. Add a real provider-specific source schema and a validated server-side
search operation. Keep authentication on the server. Extend the source
union and put dispatch at the feed boundary, outside the post card.
2. Implement normalization with stable provider identity. For Mastodon, do not
treat an instance-local numeric ID as globally unique; for Bluesky prefer
the canonical record identity; for Nostr use event identity. The human
original-post URL and the deduplication identity are separate fields.
3. Add a connection reference when per-column accounts, Mastodon instances,
or Nostr relay sets are introduced. Include it in query-cache identity.
4. Support multiple sources within a column only when a second connector can
exercise it. Each source needs its own continuation and error state. Do not
merge opaque upstream cursors into one cross-platform cursor or claim a
globally complete chronological feed from separately ranked searches.
Search capability is connection-dependent. Mastodon documents that status
search depends on the instance's search backend and authentication:
[Mastodon search API](https://docs.joinmastodon.org/methods/search/).
Nostr's full-text search is an optional relay capability:
[NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md).
Validate actual connection capabilities when those connectors are added.
Threads authorization and Bluesky endpoint behavior must likewise be verified
when implementing those adapters, not inferred from Twitter's contract.
## AI increment
The next product slice should turn a topic into validated column definitions,
then let the user refine them. AI-generated conditions use the same schema as
manual edits. Summaries should reference a persisted collection snapshot
(post keys, original URLs, retrieval time, source/query and profile context),
so a later refresh does not change the evidence behind an earlier claim.
LocalStorage of conditions alone does not provide this evidence store.
ACP or Codex app-server can connect the planner to the application, but they
are not part of the platform data model or this implementation.
## Verification
Unit tests cover normalization, source/definition validation, ordering and
storage errors. Playwright tests use the existing standalone mock relay through
the real server functions to verify independent pagination, retry, editing,
ordering, reload, deletion/undo, and invalid saved data. They also check viewport
widths 320/375/414/768 and run Axe on the populated deck.
No live SNS search is required by these tests.
+3 -1
View File
@@ -3,7 +3,9 @@
## Scope and design
Expose the existing intentional reader to agents through three React-owned
tools. Search remains available across routes. Feed tools are registered only
reader tools. The deck additionally exposes `get_deck` and `set_deck` on `/deck`;
see [deck tool contracts](research-decks.md#deck-webmcp-tools).
Search remains available across routes. Feed tools are registered only
while a valid timeline, search, or conversation is open. Returning home or to
an empty search/list page removes the feed tools. Unsupported browsers do not
register tools or fetch posts automatically.