feat(tinyplace): public profile viewer for arbitrary handles (#4931)#4997
Conversation
Agent World Profiles was hard-wired to the connected wallet, so you could only see your own profile. The read handlers for any handle were already plumbed and registered (graphql.profile / profiles.* / follows.*) but had no UI caller. Add a /agent-world/profiles/:username route and a read-only ProfileViewer that renders graphql.profile(username) for any user or agent — header, bio, skills, verified accounts, owned handles, follower stats — with a follow/unfollow button (follows.follow/unfollow, initial state from the viewer's follows.following graph) and a copy-link affordance for sharing. Read-only; profile editing (tinyhumansai#4930) is untouched.
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a public Agent World profile viewer for arbitrary usernames, including profile loading states, follow/unfollow actions, statistics, copy-link support, dynamic routing, tests, and translations across supported locales. ChangesAgent World public profile viewer
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant ProfileViewer
participant AgentWorldAPI
participant FollowsAPI
Browser->>ProfileViewer: Open /agent-world/profiles/:username
ProfileViewer->>AgentWorldAPI: Load graphql.profile(username)
ProfileViewer->>FollowsAPI: Load agent card and stats
ProfileViewer-->>Browser: Render profile and actions
Browser->>ProfileViewer: Click follow or copy link
ProfileViewer->>FollowsAPI: Follow or unfollow profile
ProfileViewer-->>Browser: Update state or clipboard link
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcc99b2842
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…4931-tinyplace-profiles-wallet # Conflicts: # app/src/lib/i18n/ar.ts # app/src/lib/i18n/bn.ts # app/src/lib/i18n/de.ts # app/src/lib/i18n/en.ts # app/src/lib/i18n/es.ts # app/src/lib/i18n/fr.ts # app/src/lib/i18n/hi.ts # app/src/lib/i18n/id.ts # app/src/lib/i18n/it.ts # app/src/lib/i18n/ko.ts # app/src/lib/i18n/pl.ts # app/src/lib/i18n/pt.ts # app/src/lib/i18n/ru.ts # app/src/lib/i18n/zh-CN.ts
|
Fixed the Frontend Checks failure (2 failing tests in
Also merged current @coderabbitai review Fixed 2 test-only failures + merged main (i18n union-resolve). |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 17
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/agentworld/pages/AgentWorld.tsx`:
- Around line 178-179: Update the active-section pathname parsing in AgentWorld
so it uses the first segment after /agent-world rather than the final segment;
ensure nested routes such as profiles/:username resolve to the profiles section
and keep the existing sidebar selection behavior for other sections.
In `@app/src/agentworld/pages/ProfileViewer.tsx`:
- Around line 40-43: Update formatDate to use the selected application locale or
the existing shared date-formatting utility instead of hard-coded 'en-US'.
Preserve the invalid-date fallback and the current year/month/day formatting
behavior.
- Around line 251-270: Extend ProfileCard to use the existing profile, activity,
group, agent-card, and follow handlers, including profile.agentCard data. Render
read-only agent-card, activity, and group-objective sections, and provide
explicit empty and failure states for activity and group data while preserving
the existing profile and follow behavior.
- Around line 140-150: Update the following-status logic around
apiClient.follows.following so it does not infer “not_following” from incomplete
or failed data. Paginate through all relationships or use a direct relationship
lookup, and set “not_following” only after a complete successful check; preserve
“unknown” on errors or incomplete lookups and provide retry/error handling
rather than enabling follow.
- Around line 103-110: Update the profile-loading error handling around the
catch block and the related error paths at the referenced locations so raw
external errors are never logged or stored in UI state. Map failures to a stable
error kind, render translated generic user-facing copy, and log only sanitized
error names or codes; preserve the special payment-required behavior without
exposing its raw message.
- Around line 82-90: The ProfileViewer effect should no longer synchronously
call setState for loading or not_found when the normalized handle changes. Key
the async profile state by the normalized handle/target, derive loading and
disabled UI state when the stored key does not match the current handle, and
ensure stale responses cannot expose the previous profile or actions; apply the
same approach to the state updates referenced around lines 134-137.
- Around line 259-276: Update ProfileViewer’s share-link flow to use the
normalized username from the requested route rather than deriving it from
profile.identities or displayName. Pass that route username into ProfileCard,
and have copyLink build the deep-link profile path from the passed normalized
handle so links resolve even when identities are null or differ from the
selected route.
- Around line 156-168: Add constant privacy-safe analytics IDs to the follow and
copy buttons, then use trackAnalyticsEvent for successful follow, unfollow, and
copy outcomes in the relevant ProfileViewer handlers, including toggle and copy
flows. Emit events only after each domain action succeeds, and include only
stable action identifiers or non-sensitive outcome data—never the target handle,
address, or other PII.
- Around line 52-66: Instrument useMyAgentId and the related wallet resolution,
relationship loading, statistics, and clipboard flows with namespaced,
grep-friendly diagnostics for external-call start, success, failure, relevant
branches, and state transitions. Replace swallowed catches with privacy-safe
error logging while excluding handles, wallet addresses, and other sensitive
values; cover the referenced sections without changing their functional
behavior.
In `@app/src/lib/i18n/fr.ts`:
- Around line 485-486: Update the French ProfileViewer localization strings
`agentWorld.profileViewer.notFoundBody` and the related metric label to use
French wording for a public username/handle instead of “identifiant,” preserving
the existing message meaning and translation style.
In `@app/src/lib/i18n/it.ts`:
- Line 482: Update the Italian translations for the following-related keys at
the shown entries, replacing imperative or verb-form wording with noun/state
labels that render correctly after a count: use “Stai già seguendo” for the
profile viewer state and “seguiti” (or the project’s established metric wording)
for the corresponding count label.
In `@app/src/lib/i18n/pl.ts`:
- Line 488: Update the translation value for the
`agentWorld.profileViewer.joined` key to a gender-neutral Polish label, choosing
`Dołączenie` or `Dołączono` based on how the value is rendered.
- Around line 476-477: Update the Polish translations for
agentWorld.profileViewer.notFoundBody and the additionally affected profile
message to replace “uchwyt” with consistent profile-handle terminology,
preferably “nazwa użytkownika” or “identyfikator,” while preserving the original
message meaning.
In `@app/src/lib/i18n/pt.ts`:
- Line 479: Update the Portuguese translation for the
`agentWorld.profileViewer.handlesOwned` key to use Agent World’s `Handle`
terminology, replacing the generic “Identificadores próprios” wording with a
phrase such as “Handles próprios” or “Handles que possui”.
In `@app/src/lib/i18n/ru.ts`:
- Line 479: Update the Russian translation for
agentWorld.profileViewer.handlesOwned to use a handle-specific label consistent
with the product terminology, such as “Собственные хэндлы,” instead of the
generic identifier wording.
- Line 482: Update the Russian translation for the
`agentWorld.profileViewer.joined` key to use a gender-neutral label, such as
“Дата присоединения”, instead of the masculine “Присоединился”.
In `@app/src/lib/i18n/zh-CN.ts`:
- Line 447: Update the agentWorld.profileViewer.agentBadge translation value
from “代理” to “智能体” to match the product’s established terminology.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 61c319ee-0e99-402e-b28a-27daf35ef668
📒 Files selected for processing (17)
app/src/agentworld/pages/AgentWorld.tsxapp/src/agentworld/pages/ProfileViewer.test.tsxapp/src/agentworld/pages/ProfileViewer.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.ts
graycyrus
left a comment
There was a problem hiding this comment.
@M3gA-Mind heads up — CI is red on this one (Frontend Checks / Vitest), so I'm holding off on a full review until it's green. Dug into the actual failure though, and it's not flaky — it's a real bug in the new test:
ProfileViewer.test.tsx:102 asserts screen.getByText('3') for the follower count synchronously, right after findByText('@alice') resolves. But useFollowStats only fires its follows.stats() call in a separate effect after ProfileCard first mounts — that's a second async round-trip beyond the profile-load wait the test already did. The assertion races the mock and loses, which is exactly what's failing in CI (Unable to find an element with the text: 3).
Separately, CodeRabbit already left a solid set of majors on this PR (synchronous state resets inside effects, swallowed error logging on external calls, follow-status inferred from a partial/failed relationship lookup, the share link being built from the wrong username source, and the activity/groups/agent-card handlers from #4931 not actually wired up — so this only closes part of the linked issue) — worth working through those in the same pass.
Fix the test (and CI) and I'll come back for a proper review.
…4931-tinyplace-profiles-wallet
- AgentWorld: derive active section from the first path segment so nested /agent-world/profiles/:username keeps the Profiles sidebar selection. - ProfileViewer: key profile state by handle and only write state in async callbacks (no synchronous setState in effects); stale responses can no longer surface the wrong profile. - Follow state now comes from the agent card's signer-aware viewerIsFollowing (a direct, complete lookup) instead of scanning a truncated following list; stays 'unknown' (button hidden) when absent rather than inferring. - Render the agent card summary read-only; build the share link from the route handle; format dates with the runtime locale; surface a generic translated error instead of the raw error string; add grep-friendly, PII-free diagnostics to previously-swallowed catches. - i18n wording fixes (fr/it/pl/pt/ru handle terminology + gender-neutral joined, zh-CN agent term) and a new agentCard label across all locales.
|
Addressed CodeRabbit's review. Fixed vs declined: Fixed
Declined (with in-thread replies)
Note for the twin PR #4996 (own-profile editing): the
@coderabbitai review Addressed review: keyed state + direct follow-flag + generic errors + share-link + i18n wording; declined analytics/activity-groups with reasons. |
|
✅ Action performedReview finished.
|
graycyrus
left a comment
There was a problem hiding this comment.
@M3gA-Mind Nice cleanup pass — most of CodeRabbit's findings are genuinely addressed here: the sidebar active-section fix for nested routes, hiding the follow button on unknown state instead of guessing "not following", sanitized error logging (no more raw error strings), locale-aware date formatting, the share link now built from the route handle instead of identities, and the i18n wording fixes all look correct.
One thing that concerns me though: the CI failure I flagged last time (ProfileViewer.test.tsx) is still there. expect(screen.getByText('3')).toBeInTheDocument() at line ~106-108 still runs synchronously right after the profile findByText resolves, but follower stats load in their own separate effect (useFollowStats) that only kicks off once ProfileCard mounts. That's a second async round-trip the test doesn't wait for — this is the same root cause as before, untouched by this commit. Frontend Checks is still running on 05e7203 but I'd expect it to fail again on this exact assertion unless something else changed. Wrap that last assertion in findByText/waitFor instead of getByText.
Also still open (not addressed in this push): CodeRabbit's "add stable, privacy-safe analytics for the new follow/copy actions" (major) and codex's note about refreshing follower counts after a follow/unfollow toggle (currently followStats is fetched once and never re-synced after follow.toggle() succeeds, so the count visibly lags the button state).
On the linked issue — #4931 asks for posts/bounties (profiles.activity/profiles.groups) in addition to what's here; this push adds the agent-card description summary but activity/groups are still deferred. Worth calling that out explicitly as a known gap if this merges, since the issue will read as fully closed otherwise.
Holding off on a full re-review until CI is green and the test fix lands — let me know when it's ready and I'll take another pass.
…ollow toggle
- ProfileViewer.test: the follower-count assertion ran synchronously right
after the profile load, but useFollowStats fires in a separate effect (a
second async tick); switch getByText('3') to findByText so it no longer
races the mock.
- ProfileViewer: the follower count was fetched once and lagged the button
after a follow/unfollow. Apply an optimistic delta on toggle so the count
tracks the action, and assert 3->4->3 in the toggle test.
|
Thanks @graycyrus — addressed both open items from your last pass. Test race (your [major], ProfileViewer.test.tsx) — FIXED. Follower count lagging after toggle (codex's note) — FIXED. Still deferred, with prior agreement:
@coderabbitai review |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== Changed files and current PR diff summary ==='
git diff --stat origin/main...HEAD
git diff --name-only origin/main...HEAD
echo '=== ProfileViewer structural outline ==='
ast-grep outline app/src/agentworld/pages/ProfileViewer.tsx --items all --view expanded || true
echo '=== ProfileViewer test structural outline ==='
ast-grep outline app/src/agentworld/pages/ProfileViewer.test.tsx --items all --view expanded || true
echo '=== Latest relevant diffs ==='
git diff --unified=80 origin/main...HEAD -- \
app/src/agentworld/pages/ProfileViewer.tsx \
app/src/agentworld/pages/ProfileViewer.test.tsx
echo '=== Follow stats / optimistic delta references with context ==='
rg -n -C 5 --glob '*.ts' --glob '*.tsx' \
'(useFollowStats|followerDelta|setFollowerDelta|follows\.stats|follows\.(follow|unfollow)|viewerIsFollowing)' \
app/src
echo '=== Agent World API follow-related declarations ==='
rg -n -C 4 --glob '*.ts' --glob '*.tsx' \
'(interface .*Follow|class .*Follow|following\s*[:(]|stats\s*[:(]|follow\s*[:(]|unfollow\s*[:(])' \
app/src/agentworld app/src/libLength of output: 87961
🐇 ✅ ✅ Action performedReview finished.
|
oxoxDev
left a comment
There was a problem hiding this comment.
Solid read-only viewer. No XSS (no dangerouslySetInnerHTML; all untrusted profile data renders as auto-escaped JSX, avatarUrl only into <img src>), null-guards + list keys throughout, and all view states handled. i18n parity is complete (18 keys × 14 locales) — not the CI cause.
Blocking:
- CI red is this PR's own racy test at
ProfileViewer.test.tsx:102:getByText('3')for the follower count runs beforeuseFollowStatsresolves (separate microtask) → useawait screen.findByText('3'). The other 379 tests pass; not i18n, not pre-existing. - Follow-state inference at
ProfileViewer.tsx:140-150:follows.following(myAgentId, {limit:500})treats "not in first 500" asnot_following, and.catch(() => setState('not_following'))collapses any fetch error to not-following. A viewer following >500 accounts (or hitting a transient error) sees a Follow button for someone they already follow, and clicking issues a redundant follow. Paginate / use a direct relationship lookup, and keep'unknown'(disabled) on error. (= CodeRabbit Major.)
Non-blocking:
- Raw
String(err)is rendered and logged in the error block — map to a stable kind + translated copy, log the sanitized code only. (= CodeRabbit.) copyLinkbuilds the share URL from the profile-derived handle, not the:usernameroute param — can diverge ifidentitiesis null. (= CodeRabbit.)- Follower count stays stale after a follow toggle (single fetch). (= Codex P2.)
- Sidebar active-state keys on the last path segment, so
/agent-world/profiles/alicedeselectsprofiles; derive from the first segment. (= CodeRabbit.) - Nits:
formatDatehardcodesen-US; swallowed.catch(()=>{})with no diagnostics;<img>has noonErrorfallback.
…o load Add an onError handler to the ProfileViewer avatar image so a broken/blocked avatarUrl degrades to the initials monogram instead of a broken image.
|
Thanks @oxoxDev. Both blocking items were already fixed before this review landed (it reviewed a stale commit); details + the one new nit below. Blocking — racy follower-count test: ALREADY FIXED (commit Blocking — follow-state inferred from a partial/failed lookup: ALREADY FIXED (prior commit). The Non-blocking — already addressed last pass: raw Non-blocking — Still deferred with prior agreement: analytics on follow/copy (cross-surface pass; CodeRabbit accepted) and
@coderabbitai review |
|
🐇🔍 ✅ Action performedReview finished.
|
|
@oxoxDev — every item is resolved in the current head ( [major] Racy follower-count test → FIXED ( [major] Follow-state inference (partial/failed lookup) → FIXED (prior commit). The Non-blocking — all addressed:
Deferred with prior agreement (CodeRabbit acknowledged): analytics on follow/copy (cross-surface pass) and
|
Summary
fetchWalletStatus()), so you could only ever see your own profile — there was no way to view another user's or an agent's profile.graphql.profile/profiles.get/activity/groups/agent_card/follows.*) but had zero UI callers — a pure viewer gap, not a plumbing gap./agent-world/profiles/:usernameroute + a read-onlyProfileViewerthat renders any handle'sGqlProfile(header, bio, skills, verified accounts, owned handles, follower stats), with a follow/unfollow button and a copy-link affordance for sharing.Problem
app/src/agentworld/pages/ProfilesSection.tsxresolves the wallet's Solana address (:100) and renders only that identity, at the fixed routeprofiles(AgentWorld.tsx:176, no:username). The capabilities to view any profile were already plumbed end-to-end and I verified the read path exists in the pinned SDK (openhuman can only bump thevendor/tinyplacepointer, never add SDK surface):vendor/tinyplace/sdk/rust/src/api/profiles.rs:44(get/activity/groups/broadcasts/attestations/agent_card, each taking ausername) and the profile GraphQL queryvendor/tinyplace/sdk/rust/src/api/graphql.rs:209(USER_PROFILE_QUERY, richGqlProfile).src/openhuman/tinyplace/manifest.rs:302(profiles.get) etc., registered atsrc/openhuman/tinyplace/schemas.rs:144-149.app/src/lib/agentworld/invokeApiClient.ts:1726(profiles.*),:2278(graphql.profile),:1994(follows.*).So this is a genuine, fixable UI wire-up gap (not BLOCKED): every layer below the UI carries the data for an arbitrary handle.
Solution
AgentWorld.tsxgainsprofiles/:username→ProfileViewer(kept alongside the own-profileprofilesroute). Deep-linkable and shareable.ProfileViewer.tsx(new, read-only): loadsgraphql.profile(username)and renders the header (avatar/handle/cryptoId/bio, verified + actor-type badges), skills, verified accounts, owned handles, and follower/following counts (follows.stats). Loading / not-found / error states.follows.follow/follows.unfollow(cryptoId); the initial state is derived from the viewer's ownfollows.followinggraph (universal — works for humans and agents, sinceGqlProfile's embeddedagentCarddoes not carryviewerIsFollowing). Hidden for one's own profile and when signed out; optimistic toggle; degrades gracefully.#/agent-world/profiles/<handle>) built from the live location.agentWorld.profileViewer.*keys inen.tsand every locale (no em dashes); grep-friendlyagentworld:profileviewerdebug logging that never logs the address/handle (no PII).Submission Checklist
ProfileViewer.test.tsx: renders an arbitrary handle, not-found state, follow→unfollow toggle, pre-selected following state, own-profile hides follow, copy-link.frontend-coverageis the authority.## Related— N/A: no matrix rows affected.Closes #NNNin the## Relatedsection.Impact
graphql.profilecall per view plusfollows.statsand (for non-self, signed-in) onefollows.followinglookup (limit 500) to seed follow-state; all on-demand.Related
/agent-world/profiles/:username; renderprofiles.activity(transaction stats) andagent_carddetail; paginate the follow-graph lookup beyond 500 for power users. Sibling: [Tiny Place] Agent World profile editing (name/bio/avatar) is unreachable — updateProfile RPC has no UI #4930 (profile editing).AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/GH-4931-tinyplace-profiles-walletfcc99b284210afb34b0b6e799ed71d69aa16a2a9Validation Run
pnpm --filter openhuman-app format:check— ran targeted prettier on all touched files (clean).pnpm typecheck— clean.ProfileViewer.test.tsxadded (not executed locally per machine disk policy; CI authoritative).Validation Blocked
command:pnpm test/pnpm test:coverageerror:local full test/build runs are disabled on this machine (disk-fill policy); CI performs full verification.impact:unit + coverage verification deferred to CI (frontend-coverage).Behavior Changes
/agent-world/profiles/:username, followed/unfollowed, and shared via a copy-link.Parity Contract
ProfilesSectionatprofilesis unchanged; the new route/component is purely additive.follows.*handlers as Directory/Feed; not-found/error/loading states mirror the existing sections.Duplicate / Superseded PR Handling
Summary by CodeRabbit