feat(web): workspace date filters + mobile terminal focus fix#4655
Conversation
Adds a "Recently created / Oldest / Name" sort and a "created within 7/30/90 days" filter to the /workspaces list, and surfaces a relative "created Nd ago" label on each row. v2Workspace.list now returns createdAt so the client can sort/filter without an extra round-trip.
The hidden input used to capture mobile keystrokes sat at the bottom of the layout viewport. On iOS Safari, focusing it triggered the keyboard- avoidance scroll, which on a viewport-height page yanked the whole terminal to the top. Move the input above the keyboard with pointer-events-none, and defensively restore the scroll position across the next two animation frames in case the OS scroll fires after focus.
|
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
|
Ready to review this PR? Stage has broken it down into 3 individual chapters for you:
Chapters generated by Stage for commit 02753cf on May 17, 2026 12:44am UTC. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds workspace creation-date filtering and sorting (new UI controls and relative timestamps) backed by an API change returning ChangesWorkspace creation time filtering and sorting
Mobile terminal input iOS scroll fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx (1)
34-65: ⚡ Quick winCancel queued scroll-restore frames to prevent stale scroll jumps.
requestAnimationFramecallbacks are currently fire-and-forget. On repeated focus attempts, older callbacks can still run and restore outdated coordinates. Consider canceling prior frame IDs before scheduling new ones, and on unmount.Proposed patch
export function MobileTerminalInput({ focusTargetRef, onSend, onFocusTerminal, enabled = true, toolbarVisibility = "mobile", }: MobileTerminalInputProps) { const textareaRef = useRef<HTMLTextAreaElement | null>(null); const isComposingRef = useRef(false); + const restoreRaf1Ref = useRef<number | null>(null); + const restoreRaf2Ref = useRef<number | null>(null); const focusKeyboardInput = useCallback(() => { if (!enabled) return; + if (restoreRaf1Ref.current !== null) { + cancelAnimationFrame(restoreRaf1Ref.current); + restoreRaf1Ref.current = null; + } + if (restoreRaf2Ref.current !== null) { + cancelAnimationFrame(restoreRaf2Ref.current); + restoreRaf2Ref.current = null; + } const scrollingElement = document.scrollingElement ?? document.documentElement; @@ - requestAnimationFrame(() => { + restoreRaf1Ref.current = requestAnimationFrame(() => { restore(); - requestAnimationFrame(restore); + restoreRaf2Ref.current = requestAnimationFrame(() => { + restore(); + restoreRaf2Ref.current = null; + }); + restoreRaf1Ref.current = null; }); }, [enabled]); + + useEffect(() => { + return () => { + if (restoreRaf1Ref.current !== null) { + cancelAnimationFrame(restoreRaf1Ref.current); + } + if (restoreRaf2Ref.current !== null) { + cancelAnimationFrame(restoreRaf2Ref.current); + } + }; + }, []);🤖 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 `@apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx` around lines 34 - 65, The focusKeyboardInput function schedules two requestAnimationFrame callbacks that can become stale; store the returned frame IDs (e.g., in a ref like restoreFrameRef) and before scheduling new frames cancel any existing IDs with cancelAnimationFrame, then schedule the two frames and save their IDs; also cancel any pending frames in a useEffect cleanup/unmount to avoid old restore() calls restoring outdated scroll coordinates. Ensure references to textareaRef and the restore inner function remain unchanged while managing the frame IDs.apps/web/src/app/workspaces/page.tsx (1)
294-314: ⚡ Quick winAdd explicit accessible labels to the new filter selects.
The newly added controls need explicit names for screen readers (
aria-labelor<label htmlFor>).Proposed fix
<select value={createdWithin} onChange={(event) => setCreatedWithin(event.target.value as CreatedWithin) } + aria-label="Filter by creation date" className="rounded-md border bg-transparent px-3 py-2 text-sm" > @@ <select value={sortBy} onChange={(event) => setSortBy(event.target.value as SortBy)} + aria-label="Sort workspaces" className="rounded-md border bg-transparent px-3 py-2 text-sm" >🤖 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 `@apps/web/src/app/workspaces/page.tsx` around lines 294 - 314, The two new select controls for filtering (the one bound to createdWithin / setCreatedWithin and the one bound to sortBy / setSortBy) lack accessible labels; add explicit accessible names by either associating each select with a <label htmlFor="..."> and matching id on the select or by adding an aria-label attribute (e.g., aria-label="Created within" for the createdWithin select and aria-label="Sort by" for the sortBy select) so screen readers can announce their purpose; ensure the chosen ids/aria-label strings are descriptive and update any type casts (CreatedWithin, SortBy) unaffected.
🤖 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 `@apps/web/src/app/workspaces/page.tsx`:
- Around line 26-38: The formatRelative function uses Math.round which causes
premature rollovers (e.g., 23.5h → 1d); change the rounding strategy to use
Math.floor for all unit conversions in formatRelative so minutes, hours, days,
months and years are computed with Math.floor (keep the initial minutes < 1
check as-is) to provide stable "N unit(s) ago" labels; update the conversions in
function formatRelative accordingly (replace Math.round with Math.floor for
minutes, hours, days, months, years).
---
Nitpick comments:
In `@apps/web/src/app/workspaces/page.tsx`:
- Around line 294-314: The two new select controls for filtering (the one bound
to createdWithin / setCreatedWithin and the one bound to sortBy / setSortBy)
lack accessible labels; add explicit accessible names by either associating each
select with a <label htmlFor="..."> and matching id on the select or by adding
an aria-label attribute (e.g., aria-label="Created within" for the createdWithin
select and aria-label="Sort by" for the sortBy select) so screen readers can
announce their purpose; ensure the chosen ids/aria-label strings are descriptive
and update any type casts (CreatedWithin, SortBy) unaffected.
In `@apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx`:
- Around line 34-65: The focusKeyboardInput function schedules two
requestAnimationFrame callbacks that can become stale; store the returned frame
IDs (e.g., in a ref like restoreFrameRef) and before scheduling new frames
cancel any existing IDs with cancelAnimationFrame, then schedule the two frames
and save their IDs; also cancel any pending frames in a useEffect
cleanup/unmount to avoid old restore() calls restoring outdated scroll
coordinates. Ensure references to textareaRef and the restore inner function
remain unchanged while managing the frame IDs.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: fa35bddc-9831-4d59-b3d4-f1815d333207
📒 Files selected for processing (3)
apps/web/src/app/workspaces/page.tsxapps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsxpackages/trpc/src/router/v2-workspace/v2-workspace.ts
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
Greptile SummaryThis PR adds sort and date-range filters to the
Confidence Score: 4/5Safe to merge; the workspace filter changes are additive client-side logic with no backend risk, and the iOS scroll fix is targeted and well-commented. The workspace page changes are straightforward client-side filtering — the tRPC change is a single field addition with no migration needed. The mobile terminal fix correctly addresses the iOS Safari keyboard-scroll bug, but the pre-existing double-firing of focusKeyboardInput means two independent scroll-restore rAF chains are scheduled on every touch event. In async-scroll scenarios this is harmless, but in synchronous-scroll scenarios the second chain could restore the page to the displaced position and defeat the fix. MobileTerminalInput.tsx deserves a second look around the double-fire of focusKeyboardInput — specifically whether the touchstart listener is still needed now that the non-mouse pointerdown branch handles touch.
|
| Filename | Overview |
|---|---|
| apps/web/src/app/workspaces/page.tsx | Adds sort (recent/oldest/name) and createdWithin (all/7d/30d/90d) filters plus a relative-time label; logic in useMemo is correct, though formatRelative timestamps freeze at render time. |
| apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx | Moves hidden textarea to top-0 and adds two-frame rAF scroll restore to defeat iOS Safari keyboard scroll; the fix is sound but the double-fire of focusKeyboardInput (touchstart + non-mouse pointerdown) could produce conflicting rAF restore chains if iOS scroll fires synchronously. |
| packages/trpc/src/router/v2-workspace/v2-workspace.ts | Minimal addition of createdAt to the list query SELECT and the returned row shape; isolated and mechanically correct. |
Sequence Diagram
sequenceDiagram
participant User
participant WorkspacesPage
participant tRPC
participant DB
User->>WorkspacesPage: Load /workspaces
WorkspacesPage->>tRPC: "v2Workspace.list({ organizationId })"
tRPC->>DB: SELECT id, name, branch, projectId, projectName, hostId, createdAt
DB-->>tRPC: rows[]
tRPC-->>WorkspacesPage: rows with createdAt
WorkspacesPage->>WorkspacesPage: map rows to WorkspaceRow new Date(createdAt)
User->>WorkspacesPage: Change sort or createdWithin filter
WorkspacesPage->>WorkspacesPage: useMemo recomputes visibleWorkspaces
WorkspacesPage-->>User: Re-rendered list with created Nd ago labels
Note over User,WorkspacesPage: Mobile terminal focus fix
User->>WorkspacesPage: Tap terminal touchstart then pointerdown
WorkspacesPage->>WorkspacesPage: focusKeyboardInput called twice
WorkspacesPage->>WorkspacesPage: save scrollTop
WorkspacesPage->>WorkspacesPage: textarea.focus preventScroll true
WorkspacesPage->>WorkspacesPage: rAF restore scroll across 2 frames
Comments Outside Diff (1)
-
apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx, line 67-88 (link)Double
focusKeyboardInputcall creates conflicting restore cyclesBoth
onTouchStartand the non-mouse branch ofonPointerDowncallfocusKeyboardInput. On iOS touch, both events fire for the same gesture —touchstartfires beforepointerdown— so two independent save/restore cycles are scheduled. If iOS's keyboard-avoidance scroll fires synchronously during the firstfocus()call, the second call captures the post-scroll (displaced) position as itssavedScrollTopand its rAF chain restores the page to that displaced position, after the first chain has already corrected it. In async-scroll scenarios this is harmless, but the code would silently regress when iOS behavior changes.Consider guarding
focusKeyboardInputwith an in-flight flag or removing the now-redundantonTouchStartlistener, sinceonPointerDownwithpointerType !== "mouse"already covers touch inputs.Prompt To Fix With AI
This is a comment left during a code review. Path: apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx Line: 67-88 Comment: **Double `focusKeyboardInput` call creates conflicting restore cycles** Both `onTouchStart` and the non-mouse branch of `onPointerDown` call `focusKeyboardInput`. On iOS touch, both events fire for the same gesture — `touchstart` fires before `pointerdown` — so two independent save/restore cycles are scheduled. If iOS's keyboard-avoidance scroll fires synchronously during the first `focus()` call, the second call captures the post-scroll (displaced) position as its `savedScrollTop` and its rAF chain restores the page to that displaced position, after the first chain has already corrected it. In async-scroll scenarios this is harmless, but the code would silently regress when iOS behavior changes. Consider guarding `focusKeyboardInput` with an in-flight flag or removing the now-redundant `onTouchStart` listener, since `onPointerDown` with `pointerType !== "mouse"` already covers touch inputs. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/web/src/components/MobileTerminalInput/MobileTerminalInput.tsx:67-88
**Double `focusKeyboardInput` call creates conflicting restore cycles**
Both `onTouchStart` and the non-mouse branch of `onPointerDown` call `focusKeyboardInput`. On iOS touch, both events fire for the same gesture — `touchstart` fires before `pointerdown` — so two independent save/restore cycles are scheduled. If iOS's keyboard-avoidance scroll fires synchronously during the first `focus()` call, the second call captures the post-scroll (displaced) position as its `savedScrollTop` and its rAF chain restores the page to that displaced position, after the first chain has already corrected it. In async-scroll scenarios this is harmless, but the code would silently regress when iOS behavior changes.
Consider guarding `focusKeyboardInput` with an in-flight flag or removing the now-redundant `onTouchStart` listener, since `onPointerDown` with `pointerType !== "mouse"` already covers touch inputs.
### Issue 2 of 2
apps/web/src/app/workspaces/page.tsx:26-39
**`formatRelative` timestamps freeze at the time of initial render**
`Date.now()` is evaluated once per call, so the "created Nd ago" labels are accurate at load time but never refresh while the user stays on the page. The `createdWithin` cutoff in `visibleWorkspaces` has the same characteristic — it is recomputed only when a dependency changes, not as wall-clock time advances. If auto-updating labels are not a requirement, a short comment noting this intentional behaviour would prevent future confusion.
Reviews (1): Last reviewed commit: "fix(web): stop terminal focus from scrol..." | Re-trigger Greptile
| function formatRelative(date: Date): string { | ||
| const diffMs = Date.now() - date.getTime(); | ||
| const minutes = Math.round(diffMs / 60_000); | ||
| if (minutes < 1) return "just now"; | ||
| if (minutes < 60) return `${minutes}m ago`; | ||
| const hours = Math.round(minutes / 60); | ||
| if (hours < 24) return `${hours}h ago`; | ||
| const days = Math.round(hours / 24); | ||
| if (days < 30) return `${days}d ago`; | ||
| const months = Math.round(days / 30); | ||
| if (months < 12) return `${months}mo ago`; | ||
| const years = Math.round(months / 12); | ||
| return `${years}y ago`; | ||
| } |
There was a problem hiding this comment.
formatRelative timestamps freeze at the time of initial render
Date.now() is evaluated once per call, so the "created Nd ago" labels are accurate at load time but never refresh while the user stays on the page. The createdWithin cutoff in visibleWorkspaces has the same characteristic — it is recomputed only when a dependency changes, not as wall-clock time advances. If auto-updating labels are not a requirement, a short comment noting this intentional behaviour would prevent future confusion.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/src/app/workspaces/page.tsx
Line: 26-39
Comment:
**`formatRelative` timestamps freeze at the time of initial render**
`Date.now()` is evaluated once per call, so the "created Nd ago" labels are accurate at load time but never refresh while the user stays on the page. The `createdWithin` cutoff in `visibleWorkspaces` has the same characteristic — it is recomputed only when a dependency changes, not as wall-clock time advances. If auto-updating labels are not a requirement, a short comment noting this intentional behaviour would prevent future confusion.
How can I resolve this? If you propose a fix, please make it concise.Math.round on the unit conversions caused boundaries like 23.5h to display as "1d ago" (rounded to 24h, failed the <24 check, then floored at the day step). Switching to Math.floor keeps each unit on its own scale until the next whole unit has actually elapsed.
* feat(web): sort and filter workspaces by created date Adds a "Recently created / Oldest / Name" sort and a "created within 7/30/90 days" filter to the /workspaces list, and surfaces a relative "created Nd ago" label on each row. v2Workspace.list now returns createdAt so the client can sort/filter without an extra round-trip. * fix(web): stop terminal focus from scrolling page to top on mobile The hidden input used to capture mobile keystrokes sat at the bottom of the layout viewport. On iOS Safari, focusing it triggered the keyboard- avoidance scroll, which on a viewport-height page yanked the whole terminal to the top. Move the input above the keyboard with pointer-events-none, and defensively restore the scroll position across the next two animation frames in case the OS scroll fires after focus. * fix(web): use Math.floor in formatRelative to avoid premature rollovers Math.round on the unit conversions caused boundaries like 23.5h to display as "1d ago" (rounded to 24h, failed the <24 check, then floored at the day step). Switching to Math.floor keeps each unit on its own scale until the next whole unit has actually elapsed.
Already integrated, superseded by current versions, or net-empty in this fork: superset-sh#3881 superset-sh#3887 superset-sh#3917 superset-sh#3925 superset-sh#3940 superset-sh#3956 superset-sh#3961 superset-sh#3974 superset-sh#4017 superset-sh#4048 superset-sh#4049 superset-sh#4055 superset-sh#4063 superset-sh#4070 superset-sh#4092 superset-sh#4110 superset-sh#4138 superset-sh#4159 superset-sh#4163 superset-sh#4164 superset-sh#4209 superset-sh#4210 superset-sh#4249 superset-sh#4349 superset-sh#4405 superset-sh#4462 superset-sh#4464 superset-sh#4494 superset-sh#4495 superset-sh#4500 superset-sh#4535 superset-sh#4541 superset-sh#4566 superset-sh#4580 superset-sh#4589 superset-sh#4593 superset-sh#4603 superset-sh#4637 superset-sh#4642 superset-sh#4655 superset-sh#4657 superset-sh#4659 superset-sh#4685 superset-sh#4692 superset-sh#4745 superset-sh#4789 superset-sh#4797 superset-sh#4824 superset-sh#4835 superset-sh#4847 superset-sh#4885 superset-sh#4896.
Summary
/workspacespage, with a relative "created Nd ago" label on each row.v2Workspace.listnow returnscreatedAt.Test plan
/workspaces— confirm new "Any time / 7d / 30d / 90d" and sort dropdowns render, filter and reorder rows, and the "created Nd ago" label appears/workspaces— verify project/host/search filters still work and compose with the new onesSummary by cubic
Adds created-date sorting and a “created Nd ago” label to the Workspaces list, fixes the mobile terminal focus jump, and corrects the relative time rounding near boundaries.
New Features
/workspaces.v2Workspace.listnow returnscreatedAtfor client-side sort/filter.Bug Fixes
Written for commit 02753cf. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Bug Fixes