Skip to content

refactor: extract relativeTime into shared agentworld util#4428

Open
bastitva0-blip wants to merge 3 commits into
tinyhumansai:mainfrom
bastitva0-blip:fix/extract-relativetime-util
Open

refactor: extract relativeTime into shared agentworld util#4428
bastitva0-blip wants to merge 3 commits into
tinyhumansai:mainfrom
bastitva0-blip:fix/extract-relativetime-util

Conversation

@bastitva0-blip

@bastitva0-blip bastitva0-blip commented Jul 2, 2026

Copy link
Copy Markdown

Summary

  • Extract duplicate relativeTime helper into app/src/agentworld/utils/relativeTime.ts
  • Remove identical local copies from FeedSection, LedgerSection, and JobsSection
  • Add unit tests covering all four output branches

Problem

  • relativeTime was copy-pasted identically in three files
  • A fix or improvement would need to be applied in three places independently

Solution

  • Single canonical implementation in agentworld/utils/relativeTime.ts
  • All three components import from the shared util
  • Zero runtime behaviour change

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved consistency of relative timestamp labels (e.g., “just now”, “45m ago”, “3d ago”) across feed, jobs, and ledger views.
  • Refactor
    • Consolidated relative time formatting into a shared utility used by multiple sections.
  • Tests
    • Added a test suite to verify relative time outputs for minute, hour, and day ranges using controlled time.

@bastitva0-blip bastitva0-blip requested a review from a team July 2, 2026 19:55
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9706c9d4-9bac-477d-9726-6ea01dc3c862

📥 Commits

Reviewing files that changed from the base of the PR and between 94ed8f2 and b5d9408.

📒 Files selected for processing (2)
  • app/src/agentworld/pages/LedgerSection.tsx
  • app/src/agentworld/utils/relativeTime.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/agentworld/utils/relativeTime.ts
  • app/src/agentworld/pages/LedgerSection.tsx

📝 Walkthrough

Walkthrough

Extracts a shared relativeTime utility with tests, then updates FeedSection, JobsSection, and LedgerSection to import it instead of using local copies.

Changes

Agent World relativeTime refactor

Layer / File(s) Summary
Shared utility and tests
app/src/agentworld/utils/relativeTime.ts, app/src/agentworld/utils/relativeTime.test.ts
Adds relativeTime(iso) for just-now, minute, hour, and day formatting, with tests covering each threshold using fake timers.
Page imports and helper removal
app/src/agentworld/pages/FeedSection.tsx, app/src/agentworld/pages/JobsSection.tsx, app/src/agentworld/pages/LedgerSection.tsx
Removes the file-local relativeTime helpers, switches each page to the shared utility import, and adjusts LedgerSection metadata keying.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I hopped through time, both near and far,
One helper now shines like a guiding star. 🐇
Three pages nibble from the same green sprig,
Less copy, more calm — and the tests are big!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR adds behavior and markup changes beyond extraction, including invalid-date handling and a LedgerSection Fragment key rewrite. Limit the patch to moving the duplicated relativeTime logic into the shared util and updating the three imports; split unrelated fixes into separate PRs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main refactor: extracting relativeTime into a shared Agent World utility.
Linked Issues check ✅ Passed The shared utility was added and all three Agent World pages now import it, satisfying the refactor requested in #4427.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/src/agentworld/pages/FeedSection.tsx (1)

429-566: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Like button has no in-flight guard, unlike Follow.

handleToggleLike (Lines 777-808) computes willLike off of the current optimistic likeState, but the Like button at Lines 533-542 has no disabled attribute tied to an in-flight flag — unlike the Follow button, which is disabled via followLoading during its request (Line 490). A rapid double-click fires likePost then unlikePost (or vice-versa) concurrently; whichever response lands last silently overwrites the reconciled state, leaving the UI showing the wrong liked/count until the next refetch.

🔧 Proposed fix: add a like-in-flight guard
+  const [likeLoading, setLikeLoading] = useState<Record<string, boolean>>({});
+
   const handleToggleLike = async (post: GqlPost) => {
+    if (likeLoading[post.postId]) return;
+    setLikeLoading((prev) => ({ ...prev, [post.postId]: true }));
     const current = likeState[post.postId] ?? {
       liked: post.viewerHasLiked,
       count: post.likeCount,
     };
     const willLike = !current.liked;
     ...
     } catch (err) {
       setLikeState((prev) => ({ ...prev, [post.postId]: current }));
       console.error("[FeedSection] like/unlike failed:", err);
+    } finally {
+      setLikeLoading((prev) => ({ ...prev, [post.postId]: false }));
     }
   };
         {myAgentId ? (
           <button
             type="button"
             onClick={() => onToggleLike(post)}
+            disabled={likeLoading[post.postId] ?? false}
             className={`flex items-center gap-1 ${

Also applies to: 533-561

🤖 Prompt for 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.

In `@app/src/agentworld/pages/FeedSection.tsx` around lines 429 - 566, The Like
action in PostCard can be triggered repeatedly while a request is still in
flight, unlike the Follow button which uses followLoading for protection. Add a
like-loading/in-flight guard in the same area that owns handleToggleLike and
likeState, then pass it into PostCard and disable the like button while a toggle
request is pending. Make sure the PostCard like button uses that flag for its
disabled state and styling so rapid clicks cannot issue overlapping
likePost/unlikePost requests and overwrite the optimistic state.
app/src/agentworld/pages/JobsSection.tsx (2)

156-193: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ClientAvatar onError fallback targets the wrong DOM node.

The <img> branch (Lines 170-186) and the initials fallback <div> (Lines 188-193) are mutually-exclusive if/else returns — they are never siblings in the rendered DOM. On image load failure, target.nextElementSibling (Line 180) will not be the initials fallback; it will be whatever element actually follows <ClientAvatar /> in the parent (e.g. the job title/content <div> in JobRow). This hides the broken image but forces an unrelated sibling's display to flex, corrupting that element's layout instead of showing the intended fallback.

🐛 Proposed fix using component state instead of manual DOM traversal
 function ClientAvatar({
   avatarUrl,
   displayName,
 }: {
   avatarUrl?: string;
   displayName: string;
 }) {
+  const [imgFailed, setImgFailed] = useState(false);
   const initials = displayName
     .split(" ")
     .map((w) => w[0] ?? "")
     .slice(0, 2)
     .join("")
     .toUpperCase();

-  if (avatarUrl) {
+  if (avatarUrl && !imgFailed) {
     return (
       <img
         src={avatarUrl}
         alt={displayName}
         className="h-7 w-7 shrink-0 rounded-full object-cover"
-        onError={(e) => {
-          // Swap to initials circle on load failure
-          const target = e.currentTarget as HTMLImageElement;
-          target.style.display = "none";
-          if (target.nextElementSibling) {
-            (target.nextElementSibling as HTMLElement).style.display = "flex";
-          }
-        }}
+        onError={() => setImgFailed(true)}
       />
     );
   }

   return (
     <div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary-100 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-400">
       {initials || "?"}
     </div>
   );
 }

Requires adding useState to the existing React import.

🤖 Prompt for 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.

In `@app/src/agentworld/pages/JobsSection.tsx` around lines 156 - 193, The
ClientAvatar fallback logic is using DOM sibling traversal in onError, but the
initials fallback is not a sibling in the rendered tree, so it hides the wrong
element. Update ClientAvatar to use component state for image load failure
instead of nextElementSibling, and render the initials fallback based on that
state; also add useState to the React import. Keep the fix localized to
ClientAvatar in JobsSection and preserve the existing avatarUrl/displayName
behavior.

1135-1167: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Duplicate fetch logic with no request sequencing — stale response can overwrite fresher state.

refetchJobs (Lines 1135-1146) re-implements the same GraphQL fetch as the mount useEffect (Lines 1148-1167), but only the effect guards against stale results via cancelled. Since the "Post a Job" button (and its onCreated={refetchJobs} callback) is enabled independently of jobsState (it doesn't wait for the initial load to finish), a fast refetchJobs() call can resolve before the slower initial mount fetch, and the initial fetch's stale response will then overwrite the fresher job list when it finally resolves.

🔧 Proposed fix: single fetch function with request-id guard, reused by the mount effect
+  const requestIdRef = useRef(0);
   const refetchJobs = useCallback(() => {
+    const requestId = ++requestIdRef.current;
     setJobsState({ status: "loading" });
     void apiClient.graphql
       .jobs({ limit: 50 })
       .then((result) => {
+        if (requestId !== requestIdRef.current) return;
         const jobs = Array.isArray(result?.jobs) ? result.jobs : [];
         setJobsState({ status: "ok", jobs });
       })
       .catch((err: unknown) => {
+        if (requestId !== requestIdRef.current) return;
         setJobsState({ status: "error", message: String(err) });
       });
   }, []);

   useEffect(() => {
-    let cancelled = false;
-    setJobsState({ status: "loading" });
-
-    void apiClient.graphql
-      .jobs({ limit: 50 })
-      .then((result) => {
-        if (cancelled) return;
-        const jobs = Array.isArray(result?.jobs) ? result.jobs : [];
-        setJobsState({ status: "ok", jobs });
-      })
-      .catch((err: unknown) => {
-        if (cancelled) return;
-        setJobsState({ status: "error", message: String(err) });
-      });
-
-    return () => {
-      cancelled = true;
-    };
-  }, []);
+    refetchJobs();
+  }, [refetchJobs]);
🤖 Prompt for 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.

In `@app/src/agentworld/pages/JobsSection.tsx` around lines 1135 - 1167, The
JobsSection fetch logic is duplicated between refetchJobs and the mount
useEffect, and the current cancellation guard only protects the effect, so a
slower initial request can overwrite a newer refetchJobs result. Refactor the
shared GraphQL jobs fetch into a single helper used by both refetchJobs and the
useEffect, and add request sequencing (for example a request id or token) so
only the latest response updates setJobsState. Keep the stale-response guard in
the JobsSection component near refetchJobs/useEffect.
🧹 Nitpick comments (2)
app/src/agentworld/pages/LedgerSection.tsx (1)

72-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

StatusBlock is duplicated across pages — same candidate for extraction as relativeTime.

This StatusBlock implementation is identical to the one in FeedSection.tsx (lines 72-87 per the graph context) and is also referenced by JobsSection.tsx. Given this PR's goal of removing copy-pasted helpers (as done for relativeTime), consider extracting StatusBlock into a shared component too, to avoid the same drift risk the issue called out.

🤖 Prompt for 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.

In `@app/src/agentworld/pages/LedgerSection.tsx` around lines 72 - 87, The
StatusBlock helper is duplicated across multiple page components, so extract it
into a shared reusable component like the existing relativeTime helper. Move the
shared rendering logic from StatusBlock into one common location, then update
LedgerSection, FeedSection, and JobsSection to import and use that shared
component while keeping the same props shape (tone, title, body).
app/src/agentworld/pages/JobsSection.tsx (1)

47-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

formatAmount and StatusBlock are duplicated verbatim in LedgerSection.tsx.

This is the same copy-paste pattern this PR is fixing for relativeTime (per the linked issue). Consider extracting formatAmount (Lines 47-55) and StatusBlock (Lines 84-99) to app/src/agentworld/utils/ alongside relativeTime to prevent divergence.

🤖 Prompt for 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.

In `@app/src/agentworld/pages/JobsSection.tsx` around lines 47 - 55, The same
copy-pasted helpers are still duplicated in JobsSection, so extract both
formatAmount and StatusBlock into a shared utility under
app/src/agentworld/utils/ and update JobsSection to import them instead of
defining them inline. Reuse the existing relativeTime utility pattern as the
model, and make sure the shared exports are then used by LedgerSection and
JobsSection so the two files no longer diverge.
🤖 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/LedgerSection.tsx`:
- Around line 312-324: The metadata list in LedgerSection’s
Object.entries(tx.metadata).map() uses a shorthand Fragment without a key, so
React still treats each iteration as unkeyed. Update the map callback to use an
explicit Fragment with the key on the fragment itself, and keep the existing
dt/dd rendering inside it. Use the LedgerSection map over tx.metadata as the
place to fix this so the list satisfies React’s key requirement.

In `@app/src/agentworld/utils/relativeTime.ts`:
- Around line 1-10: The relativeTime helper can produce "NaNd ago" when iso is
invalid because new Date(iso).getTime() becomes NaN; update relativeTime to
validate the parsed date before calculating mins/hrs/days and return a safe
fallback for malformed input. Use the relativeTime function as the entry point,
add an invalid-date guard right after parsing, and keep the existing formatting
logic unchanged for valid timestamps.

---

Outside diff comments:
In `@app/src/agentworld/pages/FeedSection.tsx`:
- Around line 429-566: The Like action in PostCard can be triggered repeatedly
while a request is still in flight, unlike the Follow button which uses
followLoading for protection. Add a like-loading/in-flight guard in the same
area that owns handleToggleLike and likeState, then pass it into PostCard and
disable the like button while a toggle request is pending. Make sure the
PostCard like button uses that flag for its disabled state and styling so rapid
clicks cannot issue overlapping likePost/unlikePost requests and overwrite the
optimistic state.

In `@app/src/agentworld/pages/JobsSection.tsx`:
- Around line 156-193: The ClientAvatar fallback logic is using DOM sibling
traversal in onError, but the initials fallback is not a sibling in the rendered
tree, so it hides the wrong element. Update ClientAvatar to use component state
for image load failure instead of nextElementSibling, and render the initials
fallback based on that state; also add useState to the React import. Keep the
fix localized to ClientAvatar in JobsSection and preserve the existing
avatarUrl/displayName behavior.
- Around line 1135-1167: The JobsSection fetch logic is duplicated between
refetchJobs and the mount useEffect, and the current cancellation guard only
protects the effect, so a slower initial request can overwrite a newer
refetchJobs result. Refactor the shared GraphQL jobs fetch into a single helper
used by both refetchJobs and the useEffect, and add request sequencing (for
example a request id or token) so only the latest response updates setJobsState.
Keep the stale-response guard in the JobsSection component near
refetchJobs/useEffect.

---

Nitpick comments:
In `@app/src/agentworld/pages/JobsSection.tsx`:
- Around line 47-55: The same copy-pasted helpers are still duplicated in
JobsSection, so extract both formatAmount and StatusBlock into a shared utility
under app/src/agentworld/utils/ and update JobsSection to import them instead of
defining them inline. Reuse the existing relativeTime utility pattern as the
model, and make sure the shared exports are then used by LedgerSection and
JobsSection so the two files no longer diverge.

In `@app/src/agentworld/pages/LedgerSection.tsx`:
- Around line 72-87: The StatusBlock helper is duplicated across multiple page
components, so extract it into a shared reusable component like the existing
relativeTime helper. Move the shared rendering logic from StatusBlock into one
common location, then update LedgerSection, FeedSection, and JobsSection to
import and use that shared component while keeping the same props shape (tone,
title, body).
🪄 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: 58956fc2-9a90-4540-807c-b06f0b37f47c

📥 Commits

Reviewing files that changed from the base of the PR and between 6e17240 and 5a8e469.

📒 Files selected for processing (5)
  • app/src/agentworld/pages/FeedSection.tsx
  • app/src/agentworld/pages/JobsSection.tsx
  • app/src/agentworld/pages/LedgerSection.tsx
  • app/src/agentworld/utils/relativeTime.test.ts
  • app/src/agentworld/utils/relativeTime.ts

Comment thread app/src/agentworld/pages/LedgerSection.tsx
Comment thread app/src/agentworld/utils/relativeTime.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] relativeTime() copy-pasted identically across FeedSection, LedgerSection, and JobsSection

1 participant