From ed9e0baf5bb57e17591f029464b8619df9297c2b Mon Sep 17 00:00:00 2001 From: Ammar Date: Tue, 2 Dec 2025 11:11:55 -0600 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20colored=20bord?= =?UTF-8?q?ers=20to=20runtime=20badges=20for=20better=20discrimination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SSH: blue theme (idle and working) - Worktree: purple theme (idle and working) - Local: gray theme (idle and working) Each runtime type maintains consistent coloring, with working state showing brighter colors and pulse animation. Added RuntimeBadgeVariations story showing all 6 states. Added createLocalWorkspace helper to mockFactory. _Generated with `mux`_ --- src/browser/components/RuntimeBadge.tsx | 39 ++++--- src/browser/stories/App.sidebar.stories.tsx | 119 ++++++++++++++++++++ src/browser/stories/mockFactory.ts | 10 ++ 3 files changed, 155 insertions(+), 13 deletions(-) diff --git a/src/browser/components/RuntimeBadge.tsx b/src/browser/components/RuntimeBadge.tsx index 93244ac994..f00f1a8470 100644 --- a/src/browser/components/RuntimeBadge.tsx +++ b/src/browser/components/RuntimeBadge.tsx @@ -78,31 +78,42 @@ function LocalIcon() { ); } +// Runtime-specific color schemes - each type has consistent colors in idle/working states +const RUNTIME_STYLES = { + ssh: { + idle: "bg-blue-500/10 text-blue-400/70 border-blue-500/40", + working: "bg-blue-500/20 text-blue-400 border-blue-500/50 animate-pulse", + }, + worktree: { + idle: "bg-purple-500/10 text-purple-400/70 border-purple-500/40", + working: "bg-purple-500/20 text-purple-400 border-purple-500/50 animate-pulse", + }, + local: { + idle: "bg-muted/20 text-muted/70 border-muted/40", + working: "bg-muted/30 text-muted border-muted/50 animate-pulse", + }, +} as const; + /** * Badge to display runtime type information. * Shows icon-only badge with tooltip describing the runtime type. - * - SSH: server icon with hostname - * - Worktree: git branch icon (isolated worktree) - * - Local: folder icon (project directory) + * - SSH: server icon with hostname (blue theme) + * - Worktree: git branch icon (purple theme) + * - Local: folder icon (gray theme) * - * When isWorking=true, badges show blue color with pulse animation. - * When idle, badges show gray styling. + * When isWorking=true, badges brighten and pulse within their color scheme. */ export function RuntimeBadge({ runtimeConfig, className, isWorking = false }: RuntimeBadgeProps) { - // Dynamic styling based on working state - const workingStyles = isWorking - ? "bg-blue-500/20 text-blue-400 border-blue-500/40 animate-pulse" - : "bg-muted/30 text-muted border-muted/50"; - // SSH runtime: show server icon with hostname if (isSSHRuntime(runtimeConfig)) { const hostname = extractSshHostname(runtimeConfig); + const styles = isWorking ? RUNTIME_STYLES.ssh.working : RUNTIME_STYLES.ssh.idle; return ( @@ -115,12 +126,13 @@ export function RuntimeBadge({ runtimeConfig, className, isWorking = false }: Ru // Worktree runtime: show git branch icon if (isWorktreeRuntime(runtimeConfig)) { + const styles = isWorking ? RUNTIME_STYLES.worktree.working : RUNTIME_STYLES.worktree.idle; return ( @@ -133,12 +145,13 @@ export function RuntimeBadge({ runtimeConfig, className, isWorking = false }: Ru // Local project-dir runtime: show folder icon if (isLocalProjectRuntime(runtimeConfig)) { + const styles = isWorking ? RUNTIME_STYLES.local.working : RUNTIME_STYLES.local.idle; return ( diff --git a/src/browser/stories/App.sidebar.stories.tsx b/src/browser/stories/App.sidebar.stories.tsx index 2f49b277a8..3c1c2fa115 100644 --- a/src/browser/stories/App.sidebar.stories.tsx +++ b/src/browser/stories/App.sidebar.stories.tsx @@ -5,8 +5,12 @@ import { appMeta, AppWithMocks, type AppStory } from "./meta.js"; import { NOW, + STABLE_TIMESTAMP, createWorkspace, createSSHWorkspace, + createLocalWorkspace, + createUserMessage, + createStreamingChatHandler, groupWorkspacesByProject, createMockAPI, installMockAPI, @@ -175,3 +179,118 @@ export const GitStatusVariations: AppStory = { /> ), }; + +/** + * All runtime badge variations showing different runtime types. + * Each type has distinct colors: + * - SSH: blue theme + * - Worktree: purple theme + * - Local: gray theme + * + * The streaming workspaces show the "working" state with pulse animation. + */ +export const RuntimeBadgeVariations: AppStory = { + render: () => ( + { + // Idle workspaces (one of each type) + const sshIdle = createSSHWorkspace({ + id: "ws-ssh-idle", + name: "ssh-idle", + projectName: "runtime-demo", + host: "dev.example.com", + createdAt: new Date(NOW - 3600000).toISOString(), + }); + const worktreeIdle = createWorkspace({ + id: "ws-worktree-idle", + name: "worktree-idle", + projectName: "runtime-demo", + createdAt: new Date(NOW - 7200000).toISOString(), + }); + const localIdle = createLocalWorkspace({ + id: "ws-local-idle", + name: "local-idle", + projectName: "runtime-demo", + createdAt: new Date(NOW - 10800000).toISOString(), + }); + + // Working workspaces (streaming - shows pulse animation) + const sshWorking = createSSHWorkspace({ + id: "ws-ssh-working", + name: "ssh-working", + projectName: "runtime-demo", + host: "prod.example.com", + createdAt: new Date(NOW - 1800000).toISOString(), + }); + const worktreeWorking = createWorkspace({ + id: "ws-worktree-working", + name: "worktree-working", + projectName: "runtime-demo", + createdAt: new Date(NOW - 900000).toISOString(), + }); + const localWorking = createLocalWorkspace({ + id: "ws-local-working", + name: "local-working", + projectName: "runtime-demo", + createdAt: new Date(NOW - 300000).toISOString(), + }); + + const workspaces = [ + sshIdle, + worktreeIdle, + localIdle, + sshWorking, + worktreeWorking, + localWorking, + ]; + + // Create streaming handlers for working workspaces + const workingMessage = createUserMessage("msg-1", "Working on task...", { + historySequence: 1, + timestamp: STABLE_TIMESTAMP, + }); + + const chatHandlers = new Map([ + [ + "ws-ssh-working", + createStreamingChatHandler({ + messages: [workingMessage], + streamingMessageId: "stream-ssh", + model: "claude-sonnet-4-20250514", + historySequence: 2, + streamText: "Processing SSH task...", + }), + ], + [ + "ws-worktree-working", + createStreamingChatHandler({ + messages: [workingMessage], + streamingMessageId: "stream-worktree", + model: "claude-sonnet-4-20250514", + historySequence: 2, + streamText: "Processing worktree task...", + }), + ], + [ + "ws-local-working", + createStreamingChatHandler({ + messages: [workingMessage], + streamingMessageId: "stream-local", + model: "claude-sonnet-4-20250514", + historySequence: 2, + streamText: "Processing local task...", + }), + ], + ]); + + installMockAPI( + createMockAPI({ + projects: groupWorkspacesByProject(workspaces), + workspaces, + chatHandlers, + }) + ); + }} + /> + ), +}; diff --git a/src/browser/stories/mockFactory.ts b/src/browser/stories/mockFactory.ts index 3de5878047..c5c407ce2e 100644 --- a/src/browser/stories/mockFactory.ts +++ b/src/browser/stories/mockFactory.ts @@ -77,6 +77,16 @@ export function createSSHWorkspace( }); } +/** Create local project-dir workspace (no isolation, uses project path directly) */ +export function createLocalWorkspace( + opts: Partial & { id: string; name: string; projectName: string } +): FrontendWorkspaceMetadata { + return createWorkspace({ + ...opts, + runtimeConfig: { type: "local" }, + }); +} + /** Create workspace with incompatible runtime (for downgrade testing) */ export function createIncompatibleWorkspace( opts: Partial & { From c3e9b21c6a9e081036824a0a5d5353550c71f1bd Mon Sep 17 00:00:00 2001 From: Ammar Date: Tue, 2 Dec 2025 11:17:06 -0600 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20ignore=20Codex=20rate?= =?UTF-8?q?=20limit=20errors=20in=20CI=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Generated with `mux`_ --- scripts/check_codex_comments.sh | 6 ++++-- .../components/GitStatusIndicatorView.tsx | 5 +++-- src/browser/components/ProjectSidebar.tsx | 3 ++- src/browser/components/RuntimeBadge.tsx | 14 ++++++++------ src/browser/contexts/WorkspaceContext.test.tsx | 3 ++- src/browser/contexts/WorkspaceContext.tsx | 8 ++++++-- src/browser/stories/App.sidebar.stories.tsx | 4 ++++ src/browser/stories/storyHelpers.ts | 17 ++++++++++++++--- src/common/constants/storage.ts | 12 ++++++++++++ 9 files changed, 55 insertions(+), 17 deletions(-) diff --git a/scripts/check_codex_comments.sh b/scripts/check_codex_comments.sh index 7f34a20989..a00c217ea9 100755 --- a/scripts/check_codex_comments.sh +++ b/scripts/check_codex_comments.sh @@ -55,8 +55,10 @@ RESULT=$(gh api graphql \ -F repo="$REPO" \ -F pr="$PR_NUMBER") -# Filter regular comments from bot that aren't minimized and don't say "Didn't find any major issues" -REGULAR_COMMENTS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.comments.nodes[] | select(.author.login == \"${BOT_LOGIN_GRAPHQL}\" and .isMinimized == false and (.body | test(\"Didn't find any major issues\") | not))]") +# Filter regular comments from bot that aren't minimized, excluding: +# - "Didn't find any major issues" (no issues found) +# - "usage limits have been reached" (rate limit error, not a real review) +REGULAR_COMMENTS=$(echo "$RESULT" | jq "[.data.repository.pullRequest.comments.nodes[] | select(.author.login == \"${BOT_LOGIN_GRAPHQL}\" and .isMinimized == false and (.body | test(\"Didn't find any major issues|usage limits have been reached\") | not))]") REGULAR_COUNT=$(echo "$REGULAR_COMMENTS" | jq 'length') # Filter unresolved review threads from bot diff --git a/src/browser/components/GitStatusIndicatorView.tsx b/src/browser/components/GitStatusIndicatorView.tsx index 5e96c844af..ce19d2b5cf 100644 --- a/src/browser/components/GitStatusIndicatorView.tsx +++ b/src/browser/components/GitStatusIndicatorView.tsx @@ -206,8 +206,9 @@ export const GitStatusIndicatorView: React.FC = ({ ); // Dynamic color based on working state - const statusColor = isWorking ? "text-blue-400 animate-pulse" : "text-muted"; - const dirtyColor = isWorking ? "text-blue-400" : "text-git-dirty"; + // Idle: muted/grayscale, Working: original accent colors + const statusColor = isWorking ? "text-accent" : "text-muted"; + const dirtyColor = isWorking ? "text-git-dirty" : "text-muted"; return ( <> diff --git a/src/browser/components/ProjectSidebar.tsx b/src/browser/components/ProjectSidebar.tsx index 283fb34b0d..aea8484a71 100644 --- a/src/browser/components/ProjectSidebar.tsx +++ b/src/browser/components/ProjectSidebar.tsx @@ -3,6 +3,7 @@ import { createPortal } from "react-dom"; import { cn } from "@/common/lib/utils"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; +import { EXPANDED_PROJECTS_KEY } from "@/common/constants/storage"; import { DndProvider } from "react-dnd"; import { HTML5Backend, getEmptyImage } from "react-dnd-html5-backend"; import { useDrag, useDrop, useDragLayer } from "react-dnd"; @@ -197,7 +198,7 @@ const ProjectSidebarInner: React.FC = ({ // Store as array in localStorage, convert to Set for usage const [expandedProjectsArray, setExpandedProjectsArray] = usePersistedState( - "expandedProjects", + EXPANDED_PROJECTS_KEY, [] ); // Handle corrupted localStorage data (old Set stored as {}) diff --git a/src/browser/components/RuntimeBadge.tsx b/src/browser/components/RuntimeBadge.tsx index f00f1a8470..5d895eb76e 100644 --- a/src/browser/components/RuntimeBadge.tsx +++ b/src/browser/components/RuntimeBadge.tsx @@ -79,18 +79,20 @@ function LocalIcon() { } // Runtime-specific color schemes - each type has consistent colors in idle/working states +// Idle: subtle with visible colored border for discrimination +// Working: brighter colors with pulse animation const RUNTIME_STYLES = { ssh: { - idle: "bg-blue-500/10 text-blue-400/70 border-blue-500/40", - working: "bg-blue-500/20 text-blue-400 border-blue-500/50 animate-pulse", + idle: "bg-transparent text-muted border-blue-500/50", + working: "bg-blue-500/20 text-blue-400 border-blue-500/60 animate-pulse", }, worktree: { - idle: "bg-purple-500/10 text-purple-400/70 border-purple-500/40", - working: "bg-purple-500/20 text-purple-400 border-purple-500/50 animate-pulse", + idle: "bg-transparent text-muted border-purple-500/50", + working: "bg-purple-500/20 text-purple-400 border-purple-500/60 animate-pulse", }, local: { - idle: "bg-muted/20 text-muted/70 border-muted/40", - working: "bg-muted/30 text-muted border-muted/50 animate-pulse", + idle: "bg-transparent text-muted border-muted/50", + working: "bg-muted/30 text-muted border-muted/60 animate-pulse", }, } as const; diff --git a/src/browser/contexts/WorkspaceContext.test.tsx b/src/browser/contexts/WorkspaceContext.test.tsx index deddfde1f0..7d51ef5156 100644 --- a/src/browser/contexts/WorkspaceContext.test.tsx +++ b/src/browser/contexts/WorkspaceContext.test.tsx @@ -11,6 +11,7 @@ import type { WorkspaceContext } from "./WorkspaceContext"; import { WorkspaceProvider, useWorkspaceContext } from "./WorkspaceContext"; import { ProjectProvider } from "@/browser/contexts/ProjectContext"; import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; +import { SELECTED_WORKSPACE_KEY } from "@/common/constants/storage"; // Helper to create test workspace metadata with default runtime config const createWorkspaceMetadata = ( @@ -649,7 +650,7 @@ describe("WorkspaceContext", () => { // Verify it's set and persisted to localStorage await waitFor(() => { expect(ctx().selectedWorkspace?.workspaceId).toBe("ws-1"); - const stored = globalThis.localStorage.getItem("selectedWorkspace"); + const stored = globalThis.localStorage.getItem(SELECTED_WORKSPACE_KEY); expect(stored).toBeTruthy(); const parsed = JSON.parse(stored!) as { workspaceId?: string }; expect(parsed.workspaceId).toBe("ws-1"); diff --git a/src/browser/contexts/WorkspaceContext.tsx b/src/browser/contexts/WorkspaceContext.tsx index 15ad6f1887..a016d64b3d 100644 --- a/src/browser/contexts/WorkspaceContext.tsx +++ b/src/browser/contexts/WorkspaceContext.tsx @@ -11,7 +11,11 @@ import { import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import type { WorkspaceSelection } from "@/browser/components/ProjectSidebar"; import type { RuntimeConfig } from "@/common/types/runtime"; -import { deleteWorkspaceStorage, migrateWorkspaceStorage } from "@/common/constants/storage"; +import { + deleteWorkspaceStorage, + migrateWorkspaceStorage, + SELECTED_WORKSPACE_KEY, +} from "@/common/constants/storage"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useProjectContext } from "@/browser/contexts/ProjectContext"; import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; @@ -107,7 +111,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) { // Manage selected workspace internally with localStorage persistence const [selectedWorkspace, setSelectedWorkspace] = usePersistedState( - "selectedWorkspace", + SELECTED_WORKSPACE_KEY, null ); diff --git a/src/browser/stories/App.sidebar.stories.tsx b/src/browser/stories/App.sidebar.stories.tsx index 3c1c2fa115..54c00be3de 100644 --- a/src/browser/stories/App.sidebar.stories.tsx +++ b/src/browser/stories/App.sidebar.stories.tsx @@ -16,6 +16,7 @@ import { installMockAPI, type GitStatusFixture, } from "./mockFactory"; +import { expandProjects } from "./storyHelpers"; export default { ...appMeta, @@ -290,6 +291,9 @@ export const RuntimeBadgeVariations: AppStory = { chatHandlers, }) ); + + // Expand the project so badges are visible + expandProjects(["/home/user/projects/runtime-demo"]); }} /> ), diff --git a/src/browser/stories/storyHelpers.ts b/src/browser/stories/storyHelpers.ts index 3b555e69c3..522d253d1d 100644 --- a/src/browser/stories/storyHelpers.ts +++ b/src/browser/stories/storyHelpers.ts @@ -8,6 +8,12 @@ import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import type { MuxMessage } from "@/common/types/message"; import type { WorkspaceChatMessage } from "@/common/types/ipc"; +import { + SELECTED_WORKSPACE_KEY, + EXPANDED_PROJECTS_KEY, + getInputKey, + getModelKey, +} from "@/common/constants/storage"; import { createWorkspace, createMockAPI, @@ -25,7 +31,7 @@ import { /** Set localStorage to select a workspace */ export function selectWorkspace(workspace: FrontendWorkspaceMetadata): void { localStorage.setItem( - "selectedWorkspace", + SELECTED_WORKSPACE_KEY, JSON.stringify({ workspaceId: workspace.id, projectPath: workspace.projectPath, @@ -37,12 +43,17 @@ export function selectWorkspace(workspace: FrontendWorkspaceMetadata): void { /** Set input text for a workspace */ export function setWorkspaceInput(workspaceId: string, text: string): void { - localStorage.setItem(`input:${workspaceId}`, text); + localStorage.setItem(getInputKey(workspaceId), text); } /** Set model for a workspace */ export function setWorkspaceModel(workspaceId: string, model: string): void { - localStorage.setItem(`model:${workspaceId}`, model); + localStorage.setItem(getModelKey(workspaceId), model); +} + +/** Expand projects in the sidebar */ +export function expandProjects(projectPaths: string[]): void { + localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify(projectPaths)); } // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 8a0054e947..26d0e22435 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -36,6 +36,18 @@ export const GLOBAL_SCOPE_ID = "__global__"; */ export const UI_THEME_KEY = "uiTheme"; +/** + * Get the localStorage key for the currently selected workspace (global) + * Format: "selectedWorkspace" + */ +export const SELECTED_WORKSPACE_KEY = "selectedWorkspace"; + +/** + * Get the localStorage key for expanded projects in sidebar (global) + * Format: "expandedProjects" + */ +export const EXPANDED_PROJECTS_KEY = "expandedProjects"; + /** * Helper to create a thinking level storage key for a workspace * Format: "thinkingLevel:{workspaceId}"