-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(mobile): pin the workspace status above the thread list #5023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { ActivityIndicator, Pressable, View } from "react-native"; | ||
|
|
||
| import { SymbolView, type SFSymbol } from "../../components/AppSymbol"; | ||
| import { AppText as Text } from "../../components/AppText"; | ||
| import { useThemeColor } from "../../lib/useThemeColor"; | ||
| import type { WorkspaceState } from "../../state/workspaceModel"; | ||
| import { useSettledWorkspaceStatusBusy } from "./use-settled-workspace-status"; | ||
| import { workspaceSyncStatusLabel } from "./workspace-connection-status"; | ||
|
|
||
| /** Leading glyph for a non-busy status; busy states render a spinner instead. */ | ||
| function workspaceStatusSymbol(state: WorkspaceState): SFSymbol { | ||
| if (state.networkStatus === "offline") return "wifi.slash"; | ||
| if (state.connectionError !== null || !state.hasReadyEnvironment) { | ||
| return "exclamationmark.triangle"; | ||
| } | ||
| return "checkmark.circle"; | ||
| } | ||
|
|
||
| /** | ||
| * Workspace connection / thread-sync status, pinned above the thread list. | ||
| * | ||
| * This deliberately sits *outside* the list's scroll view. It used to be the | ||
| * list's ListHeaderComponent — row 0 — so every sync-state flip mounted and | ||
| * unmounted a row, flashing the status and shoving every thread below it down | ||
| * and back. Pinned and always mounted, only its contents change. | ||
| * | ||
| * It also never goes away: at rest it reports what synced and when, so the bar | ||
| * answers "am I up to date?" on sight instead of only appearing mid-sync. The | ||
| * busy/quiet transition is settled first (see useSettledWorkspaceStatusBusy) | ||
| * because the underlying sync signal toggles faster than anyone can read. | ||
| */ | ||
| export function WorkspaceStatusBar(props: { | ||
| readonly state: WorkspaceState; | ||
| readonly onPress: () => void; | ||
| /** Gutter matching the surrounding list. */ | ||
| readonly className?: string; | ||
| }) { | ||
| const isBusy = useSettledWorkspaceStatusBusy(props.state); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium The status bar settles the busy/quiet spinner but leaves the label and icon un-settled, so a sub-400ms sync blip still flashes the text and glyph. 🤖 Copy this AI Prompt to have your agent fix this: |
||
| const iconColor = useThemeColor("--color-icon-muted"); | ||
| const label = workspaceSyncStatusLabel(props.state); | ||
|
|
||
| return ( | ||
| <View className={props.className}> | ||
| <Pressable | ||
| accessibilityHint="Opens environment settings" | ||
| accessibilityLabel={label} | ||
| accessibilityRole="button" | ||
| onPress={props.onPress} | ||
| className="flex-row items-center gap-2 rounded-xl bg-subtle px-3 py-2.5 active:opacity-70" | ||
| > | ||
| {isBusy ? ( | ||
| <ActivityIndicator color={iconColor} size="small" /> | ||
| ) : ( | ||
| <SymbolView | ||
| name={workspaceStatusSymbol(props.state)} | ||
| size={15} | ||
| tintColor={iconColor} | ||
| type="monochrome" | ||
| /> | ||
| )} | ||
| <Text className="min-w-0 flex-1 text-sm font-t3-bold text-foreground" numberOfLines={1}> | ||
| {label} | ||
| </Text> | ||
| <SymbolView name="chevron.right" size={11} tintColor={iconColor} type="monochrome" /> | ||
| </Pressable> | ||
| </View> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| import { | ||
| planWorkspaceStatusSettlement, | ||
| STATUS_BUSY_ENTER_DELAY_MS, | ||
| } from "./use-settled-workspace-status"; | ||
|
|
||
| describe("workspace status settlement", () => { | ||
| it("delays going busy instead of switching immediately", () => { | ||
| // The sub-second sync blips a healthy connection produces never survive | ||
| // this delay — which is the point; they used to strobe the indicator. | ||
| expect( | ||
| planWorkspaceStatusSettlement({ | ||
| rawBusy: true, | ||
| settledBusy: false, | ||
| holdUntil: 0, | ||
| now: 1_000, | ||
| }), | ||
| ).toEqual({ kind: "schedule", busy: true, delayMs: STATUS_BUSY_ENTER_DELAY_MS }); | ||
| }); | ||
|
|
||
| it("does nothing while the shown state already matches", () => { | ||
| expect( | ||
| planWorkspaceStatusSettlement({ | ||
| rawBusy: true, | ||
| settledBusy: true, | ||
| holdUntil: 5_000, | ||
| now: 1_000, | ||
| }), | ||
| ).toEqual({ kind: "hold" }); | ||
| }); | ||
|
|
||
| it("goes quiet immediately once the minimum-visible hold has elapsed", () => { | ||
| expect( | ||
| planWorkspaceStatusSettlement({ | ||
| rawBusy: false, | ||
| settledBusy: true, | ||
| holdUntil: 900, | ||
| now: 1_000, | ||
| }), | ||
| ).toEqual({ kind: "apply", busy: false }); | ||
| }); | ||
|
|
||
| it("defers going quiet for the remainder of the hold", () => { | ||
| // Shown at t=1000 with a 900ms floor; a sync that resolves at t=1200 still | ||
| // leaves the busy status up for the remaining 700ms rather than blinking. | ||
| expect( | ||
| planWorkspaceStatusSettlement({ | ||
| rawBusy: false, | ||
| settledBusy: true, | ||
| holdUntil: 1_900, | ||
| now: 1_200, | ||
| }), | ||
| ).toEqual({ kind: "schedule", busy: false, delayMs: 700 }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pre-glass iOS status double offset
High Severity
The
WorkspaceStatusBaron iOS unconditionally appliesnavigationHeaderHeightas top padding. This offset is only necessary when the navigation header is transparent. On iOS devices with an opaque header, this creates an unintended large empty gap above the status bar.Reviewed by Cursor Bugbot for commit 5c285be. Configure here.