| author | |
| committer | |
| log | 7dc182337cfddf3de1a3b2c2e1ccae810f60901a |
| tree | 9314a4fe0f31ab5e4780008753357a2474360563 |
| parent | 13917df1fe48890f794daad8c2d94f124b18b9fe |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 59 insertions(+), 2 deletions(-)
apps/web/src/components/Sidebar.logic.test.ts+39| ... | @@ -14,6 +14,7 @@ import { | ... | @@ -14,6 +14,7 @@ import { |
| 14 | resolveProjectStatusIndicator, | 14 | resolveProjectStatusIndicator, |
| 15 | resolveSidebarNewThreadSeedContext, | 15 | resolveSidebarNewThreadSeedContext, |
| 16 | resolveSidebarNewThreadEnvMode, | 16 | resolveSidebarNewThreadEnvMode, |
| 17 | resolveSidebarStageBadgeLabel, | ||
| 17 | resolveThreadRowClassName, | 18 | resolveThreadRowClassName, |
| 18 | resolveThreadStatusPill, | 19 | resolveThreadStatusPill, |
| 19 | shouldClearThreadSelectionOnMouseDown, | 20 | shouldClearThreadSelectionOnMouseDown, |
| ... | @@ -36,6 +37,44 @@ import { | ... | @@ -36,6 +37,44 @@ import { |
| 36 | 37 | ||
| 37 | const localEnvironmentId = EnvironmentId.make("environment-local"); | 38 | const localEnvironmentId = EnvironmentId.make("environment-local"); |
| 38 | 39 | ||
| 40 | describe("resolveSidebarStageBadgeLabel", () => { | ||
| 41 | it("returns Nightly for nightly primary server versions", () => { | ||
| 42 | expect( | ||
| 43 | resolveSidebarStageBadgeLabel({ | ||
| 44 | primaryServerVersion: "0.0.28-nightly.20260616.12", | ||
| 45 | fallbackStageLabel: "Alpha", | ||
| 46 | }), | ||
| 47 | ).toBe("Nightly"); | ||
| 48 | }); | ||
| 49 | |||
| 50 | it("returns the fallback label for stable primary server versions", () => { | ||
| 51 | expect( | ||
| 52 | resolveSidebarStageBadgeLabel({ | ||
| 53 | primaryServerVersion: "0.0.27", | ||
| 54 | fallbackStageLabel: "Alpha", | ||
| 55 | }), | ||
| 56 | ).toBe("Alpha"); | ||
| 57 | }); | ||
| 58 | |||
| 59 | it("returns the fallback label when the primary server version is missing", () => { | ||
| 60 | expect( | ||
| 61 | resolveSidebarStageBadgeLabel({ | ||
| 62 | primaryServerVersion: null, | ||
| 63 | fallbackStageLabel: "Dev", | ||
| 64 | }), | ||
| 65 | ).toBe("Dev"); | ||
| 66 | }); | ||
| 67 | |||
| 68 | it("returns the fallback label for malformed nightly prerelease versions", () => { | ||
| 69 | expect( | ||
| 70 | resolveSidebarStageBadgeLabel({ | ||
| 71 | primaryServerVersion: "0.0.28-nightly.20260616", | ||
| 72 | fallbackStageLabel: "Alpha", | ||
| 73 | }), | ||
| 74 | ).toBe("Alpha"); | ||
| 75 | }); | ||
| 76 | }); | ||
| 77 | |||
| 39 | function makeLatestTurn(overrides?: { | 78 | function makeLatestTurn(overrides?: { |
| 40 | completedAt?: string | null; | 79 | completedAt?: string | null; |
| 41 | startedAt?: string | null; | 80 | startedAt?: string | null; |
apps/web/src/components/Sidebar.logic.ts+11| ... | @@ -12,6 +12,7 @@ import { isLatestTurnSettled } from "../session-logic"; | ... | @@ -12,6 +12,7 @@ import { isLatestTurnSettled } from "../session-logic"; |
| 12 | 12 | ||
| 13 | export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; | 13 | export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; |
| 14 | export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100; | 14 | export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100; |
| 15 | const NIGHTLY_SERVER_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; | ||
| 15 | // Visible sidebar rows are prewarmed into the thread-detail cache so opening a | 16 | // Visible sidebar rows are prewarmed into the thread-detail cache so opening a |
| 16 | // nearby thread usually reuses an already-hot subscription. | 17 | // nearby thread usually reuses an already-hot subscription. |
| 17 | export const SIDEBAR_THREAD_PREWARM_LIMIT = 10; | 18 | export const SIDEBAR_THREAD_PREWARM_LIMIT = 10; |
| ... | @@ -64,6 +65,16 @@ export interface ThreadJumpHintVisibilityController { | ... | @@ -64,6 +65,16 @@ export interface ThreadJumpHintVisibilityController { |
| 64 | dispose: () => void; | 65 | dispose: () => void; |
| 65 | } | 66 | } |
| 66 | 67 | ||
| 68 | export function resolveSidebarStageBadgeLabel(input: { | ||
| 69 | primaryServerVersion: string | null | undefined; | ||
| 70 | fallbackStageLabel: string; | ||
| 71 | }): string { | ||
| 72 | return input.primaryServerVersion && | ||
| 73 | NIGHTLY_SERVER_VERSION_PATTERN.test(input.primaryServerVersion) | ||
| 74 | ? "Nightly" | ||
| 75 | : input.fallbackStageLabel; | ||
| 76 | } | ||
| 77 | |||
| 67 | export function createThreadJumpHintVisibilityController(input: { | 78 | export function createThreadJumpHintVisibilityController(input: { |
| 68 | delayMs: number; | 79 | delayMs: number; |
| 69 | onVisibilityChange: (visible: boolean) => void; | 80 | onVisibilityChange: (visible: boolean) => void; |
apps/web/src/components/Sidebar.tsx+9-2| ... | @@ -188,6 +188,7 @@ import { | ... | @@ -188,6 +188,7 @@ import { |
| 188 | orderItemsByPreferredIds, | 188 | orderItemsByPreferredIds, |
| 189 | shouldClearThreadSelectionOnMouseDown, | 189 | shouldClearThreadSelectionOnMouseDown, |
| 190 | sortProjectsForSidebar, | 190 | sortProjectsForSidebar, |
| 191 | resolveSidebarStageBadgeLabel, | ||
| 191 | useThreadJumpHintVisibility, | 192 | useThreadJumpHintVisibility, |
| 192 | ThreadStatusPill, | 193 | ThreadStatusPill, |
| 193 | } from "./Sidebar.logic"; | 194 | } from "./Sidebar.logic"; |
| ... | @@ -197,7 +198,7 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; | ... | @@ -197,7 +198,7 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 197 | import { useIsMobile } from "~/hooks/useMediaQuery"; | 198 | import { useIsMobile } from "~/hooks/useMediaQuery"; |
| 198 | import { CommandDialogTrigger } from "./ui/command"; | 199 | import { CommandDialogTrigger } from "./ui/command"; |
| 199 | import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; | 200 | import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; |
| 200 | import { primaryServerKeybindingsAtom } from "../state/server"; | 201 | import { primaryServerConfigAtom, primaryServerKeybindingsAtom } from "../state/server"; |
| 201 | import { | 202 | import { |
| 202 | derivePhysicalProjectKey, | 203 | derivePhysicalProjectKey, |
| 203 | deriveProjectGroupingOverrideKey, | 204 | deriveProjectGroupingOverrideKey, |
| ... | @@ -2680,6 +2681,12 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ | ... | @@ -2680,6 +2681,12 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 2680 | }: { | 2681 | }: { |
| 2681 | isElectron: boolean; | 2682 | isElectron: boolean; |
| 2682 | }) { | 2683 | }) { |
| 2684 | const primaryServerVersion = | ||
| 2685 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; | ||
| 2686 | const stageBadgeLabel = resolveSidebarStageBadgeLabel({ | ||
| 2687 | primaryServerVersion, | ||
| 2688 | fallbackStageLabel: APP_STAGE_LABEL, | ||
| 2689 | }); | ||
| 2683 | const wordmark = ( | 2690 | const wordmark = ( |
| 2684 | <div className="flex items-center gap-2"> | 2691 | <div className="flex items-center gap-2"> |
| 2685 | <SidebarTrigger className="shrink-0 md:hidden" /> | 2692 | <SidebarTrigger className="shrink-0 md:hidden" /> |
| ... | @@ -2696,7 +2703,7 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ | ... | @@ -2696,7 +2703,7 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 2696 | Code | 2703 | Code |
| 2697 | </span> | 2704 | </span> |
| 2698 | <span className="rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60"> | 2705 | <span className="rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60"> |
| 2699 | {APP_STAGE_LABEL} | 2706 | {stageBadgeLabel} |
| 2700 | </span> | 2707 | </span> |
| 2701 | </Link> | 2708 | </Link> |
| 2702 | } | 2709 | } |