Repository navigation
Messages: tags — compose, chips, filter, autocomplete, trending - #155
Merged
Merged
Conversation
Tags were already on the Message wire type but nothing composed, rendered or
filtered on them. Adds the two tag endpoints, the compose input, clickable
chips, and a trending panel.
- Services/InterlinedApiClient.Tags.cs — GetTrendingTagsAsync and
AutocompleteTagsAsync. Both wrap their rows under { "tags": [...] } and both
honor ?limit= (live-verified). Autocomplete short-circuits a blank query: an
empty q is a 400 ("Query parameter 'q' is required"), so there is no point
round-tripping into an error the caller has to swallow.
- Models/TrendingTag.cs — Tag/Count/LastUsedAt. The two endpoints' Count values
are NOT the same number (trending counts a trailing window, autocomplete
counts all-time: "thoughts" was 1 vs 5 live), which the doc comment records so
nobody treats one as a cache of the other.
- GetFeedPageAsync(limit, offset, tag) in the Messages partial — a distinct name
rather than an overload, because GetMessagesAsync in InterlinedApiClient.cs
can't be edited (open PR #130) and its optional parameters would have made an
added tag overload ambiguous.
- NewMessage gains Tags, posted as tags: string[].
- FeedViewModel: ComposeTags chips, a debounced (250ms) prefix autocomplete
whose superseded requests are cancelled rather than awaited, ActiveTag +
FilterByTag/ClearTagFilter, and TrendingTags.
- FeedView.xaml: a tag chip style, the composer's chip list + input (Enter adds,
commas split) + suggestion chips, clickable chips on every card, and a
trending bar. The trending panel is inside FeedView, not the shell's right
rail, because MainWindow.xaml is in open PR #149.
Tag input splits on commas only, never whitespace: live tags contain spaces
("orbit culture", "life is short, o brave girl"), so they are not
hashtag-shaped single words.
Live-verified read-only 2026-09-16: autocomplete is a case-insensitive literal
prefix match (q=le -> Lego, learning; q=ego -> nothing, though Lego contains
it); ?tag= is case-insensitive, pages with offset/limit, and handles
space-containing tags once URL-encoded; 57 of 80 sampled messages carry tags,
up to 6 each.
The compose write path (tags on POST /api/messages) is built and deliberately
unexercised — posting from this account publishes publicly and syndicates on.
Closes #32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR — base is
issue-31-push-quote(#152), notmainSecond of three stacked PRs (#31 → #32 → #33). All three touch
FeedView.xaml/FeedViewModel.cs/MessageItemViewModel.cs, so they aresequential rather than parallel. This one builds on #152's
Models/NewMessage.cs(it addsTagsto that record) and on the sameissue-127-wire-typesbase underneath, which is whereMessage.Tagslives.Merge order: #131 → #152 → this.
What this does
Messagealready carriedTags; nothing composed, rendered or filtered onthem.
Service — new
Services/InterlinedApiClient.Tags.cs:GetTrendingTagsAsync/AutocompleteTagsAsync. Both wrap rows under{ "tags": [ … ] }and both honor?limit=.AutocompleteTagsAsyncshort-circuits a blank query. An emptyqis a400 {"error":"Query parameter 'q' is required"}, so there is no pointround-tripping into an error the caller would have to swallow.
Models/TrendingTag.cs. Worth knowing: the two endpoints'Countvaluesare not the same number — trending counts uses within its trailing window,
autocomplete counts all-time (live:
thoughtswas1from trending and5from autocomplete). The doc comment says so, so nobody treats one as a cache
of the other.
GetFeedPageAsync(limit, offset, tag)went into theMessagespartial ratherthan becoming an overload of
GetMessagesAsync: that method is inInterlinedApiClient.cs, which is off-limits (open PR #130), and its optionalparameters would have made an added
tagoverload ambiguous at the call site.ViewModel —
ComposeTagschips,ActiveTag+FilterByTag/ClearTagFilter,TrendingTags, and a debounced (250 ms) prefix autocomplete.The autocomplete is fire-and-forget by design — it must not block the
dispatcher, and a keystroke that supersedes an in-flight request cancels it
rather than awaiting it.
View — a tag chip style, the composer's chip list + input (Enter adds, commas
split) + suggestion chips, clickable chips on every feed card, and a trending
bar.
The trending panel is inside
FeedView, not the shell's right rail, becauseMainWindow.xamlis in open PR #149 and editing it would have guaranteed aconflict. It reads fine as a horizontal chip bar between the composer and the
list, and the feed is the only place tags mean anything anyway.
Tags are not hashtags
Tag input splits on commas only, never whitespace. Live tags contain spaces
—
orbit culture,light rail,life is short, o brave girl— so awhitespace delimiter would shred them. Same reason the
?tag=filterURL-encodes.
Live verification (read-only, 2026-09-16)
GET /api/tags/trending→ 20 rows with{tag, count, lastUsedAt};?limit=3honored.
GET /api/tags/autocomplete?q=is a case-insensitive literal prefix match:q=le→Lego,learning;q=L→lists,llms,launch,Lego, …;q=ego→ nothing, even thoughLegocontains it. Emptyq→400.?limit=3honored.GET /api/messages?tag=is case-insensitive (tag=terrormatched), pages withoffset/limitexactly like the unfiltered feed, and handles aspace-containing tag (
tag=orbit%20culture→ 2 rows) once encoded.Left unexercised, on purpose
The compose write path —
tags: string[]onPOST /api/messages— is built andnot live-verified. Posting from this account publishes publicly and
syndicates to Mastodon/Bluesky. All the read paths above were exercised freely.
Also not built here: the AI Suggest tags action the issue mentions as a
pairing.
UseTagSuggestionCommandis the seam it would call into — an acceptedsuggestion is just another
AddTagValue.Notes for other open branches
MainWindow.xaml(PR #149) untouched, deliberately. The card-bodyTextBlockthat PR #141's
MentionTextwill claim is unchanged by this PR (#152 is wherethat reconcile lands).
Builds clean in both
-c Debugand-c Release.Closes #32
🤖 Generated with Claude Code