Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions scripts/check_codex_comments.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/browser/components/GitStatusIndicatorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,9 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
);

// 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 (
<>
Expand Down
3 changes: 2 additions & 1 deletion src/browser/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -197,7 +198,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({

// Store as array in localStorage, convert to Set for usage
const [expandedProjectsArray, setExpandedProjectsArray] = usePersistedState<string[]>(
"expandedProjects",
EXPANDED_PROJECTS_KEY,
[]
);
// Handle corrupted localStorage data (old Set stored as {})
Expand Down
41 changes: 28 additions & 13 deletions src/browser/components/RuntimeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,31 +78,44 @@ 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-transparent text-muted border-blue-500/50",
working: "bg-blue-500/20 text-blue-400 border-blue-500/60 animate-pulse",
},
worktree: {
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-transparent text-muted border-muted/50",
working: "bg-muted/30 text-muted border-muted/60 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 (
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
styles,
className
)}
>
Expand All @@ -115,12 +128,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 (
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
styles,
className
)}
>
Expand All @@ -133,12 +147,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 (
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
styles,
className
)}
>
Expand Down
3 changes: 2 additions & 1 deletion src/browser/contexts/WorkspaceContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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");
Expand Down
8 changes: 6 additions & 2 deletions src/browser/contexts/WorkspaceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -107,7 +111,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {

// Manage selected workspace internally with localStorage persistence
const [selectedWorkspace, setSelectedWorkspace] = usePersistedState<WorkspaceSelection | null>(
"selectedWorkspace",
SELECTED_WORKSPACE_KEY,
null
);

Expand Down
123 changes: 123 additions & 0 deletions src/browser/stories/App.sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
import { appMeta, AppWithMocks, type AppStory } from "./meta.js";
import {
NOW,
STABLE_TIMESTAMP,
createWorkspace,
createSSHWorkspace,
createLocalWorkspace,
createUserMessage,
createStreamingChatHandler,
groupWorkspacesByProject,
createMockAPI,
installMockAPI,
type GitStatusFixture,
} from "./mockFactory";
import { expandProjects } from "./storyHelpers";

export default {
...appMeta,
Expand Down Expand Up @@ -175,3 +180,121 @@ 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: () => (
<AppWithMocks
setup={() => {
// 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,
})
);

// Expand the project so badges are visible
expandProjects(["/home/user/projects/runtime-demo"]);
}}
/>
),
};
10 changes: 10 additions & 0 deletions src/browser/stories/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ export function createSSHWorkspace(
});
}

/** Create local project-dir workspace (no isolation, uses project path directly) */
export function createLocalWorkspace(
opts: Partial<WorkspaceFixture> & { 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<WorkspaceFixture> & {
Expand Down
17 changes: 14 additions & 3 deletions src/browser/stories/storyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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));
}

// ═══════════════════════════════════════════════════════════════════════════════
Expand Down
12 changes: 12 additions & 0 deletions src/common/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down