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
4 changes: 4 additions & 0 deletions src/browser/components/GitStatusIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface GitStatusIndicatorProps {
gitStatus: GitStatus | null;
workspaceId: string;
tooltipPosition?: "right" | "bottom";
/** When true, shows blue pulsing styling to indicate agent is working */
isWorking?: boolean;
}

/**
Expand All @@ -18,6 +20,7 @@ export const GitStatusIndicator: React.FC<GitStatusIndicatorProps> = ({
gitStatus,
workspaceId,
tooltipPosition = "right",
isWorking = false,
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipCoords, setTooltipCoords] = useState<{ top: number; left: number }>({
Expand Down Expand Up @@ -118,6 +121,7 @@ export const GitStatusIndicator: React.FC<GitStatusIndicatorProps> = ({
onTooltipMouseEnter={handleTooltipMouseEnter}
onTooltipMouseLeave={handleTooltipMouseLeave}
onContainerRef={handleContainerRef}
isWorking={isWorking}
/>
);
};
14 changes: 12 additions & 2 deletions src/browser/components/GitStatusIndicatorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface GitStatusIndicatorViewProps {
onTooltipMouseEnter: () => void;
onTooltipMouseLeave: () => void;
onContainerRef: (el: HTMLSpanElement | null) => void;
/** When true, shows blue pulsing styling to indicate agent is working */
isWorking?: boolean;
}

/**
Expand All @@ -57,6 +59,7 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
onTooltipMouseEnter,
onTooltipMouseLeave,
onContainerRef,
isWorking = false,
}) => {
// Handle null gitStatus (loading state)
if (!gitStatus) {
Expand Down Expand Up @@ -202,13 +205,20 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
</div>
);

// 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";

return (
<>
<span
ref={onContainerRef}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className="text-accent relative mr-1.5 flex items-center gap-1 font-mono text-[11px]"
className={cn(
"relative mr-1.5 flex items-center gap-1 font-mono text-[11px] transition-colors",
statusColor
)}
>
{gitStatus.ahead > 0 && (
<span className="flex items-center font-normal">↑{gitStatus.ahead}</span>
Expand All @@ -217,7 +227,7 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
<span className="flex items-center font-normal">↓{gitStatus.behind}</span>
)}
{gitStatus.dirty && (
<span className="text-git-dirty flex items-center leading-none font-normal">*</span>
<span className={cn("flex items-center leading-none font-normal", dirtyColor)}>*</span>
)}
</span>

Expand Down
44 changes: 33 additions & 11 deletions src/browser/components/RuntimeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { TooltipWrapper, Tooltip } from "./Tooltip";
interface RuntimeBadgeProps {
runtimeConfig?: RuntimeConfig;
className?: string;
/** When true, shows blue pulsing styling to indicate agent is working */
isWorking?: boolean;
}

/** Server rack icon for SSH runtime */
Expand Down Expand Up @@ -56,8 +58,8 @@ function WorktreeIcon() {
);
}

/** Folder icon for local project-dir runtime (reserved for future use) */
function _LocalIcon() {
/** Folder icon for local project-dir runtime */
function LocalIcon() {
return (
<svg
width="10"
Expand All @@ -81,18 +83,26 @@ function _LocalIcon() {
* 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, no badge shown by default)
* - Local: folder icon (project directory)
*
* When isWorking=true, badges show blue color with pulse animation.
* When idle, badges show gray styling.
*/
export function RuntimeBadge({ runtimeConfig, className }: RuntimeBadgeProps) {
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);
return (
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5",
"bg-accent/10 text-accent border border-accent/30",
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
className
)}
>
Expand All @@ -109,8 +119,8 @@ export function RuntimeBadge({ runtimeConfig, className }: RuntimeBadgeProps) {
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5",
"bg-muted/50 text-muted-foreground border border-muted",
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
className
)}
>
Expand All @@ -121,10 +131,22 @@ export function RuntimeBadge({ runtimeConfig, className }: RuntimeBadgeProps) {
);
}

// Local project-dir runtime: don't show badge (it's the simplest/default)
// Could optionally show LocalIcon if we want visibility
// Local project-dir runtime: show folder icon
if (isLocalProjectRuntime(runtimeConfig)) {
return null; // No badge for simple local runtimes
return (
<TooltipWrapper inline>
<span
className={cn(
"inline-flex items-center rounded px-1 py-0.5 border transition-colors",
workingStyles,
className
)}
>
<LocalIcon />
</span>
<Tooltip align="right">Local: project directory</Tooltip>
</TooltipWrapper>
);
}

return null;
Expand Down
7 changes: 4 additions & 3 deletions src/browser/components/WorkspaceHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { RuntimeBadge } from "./RuntimeBadge";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { useGitStatus } from "@/browser/stores/GitStatusStore";
import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import type { RuntimeConfig } from "@/common/types/runtime";
import { WorkspaceStatusDot } from "./WorkspaceStatusDot";
import { useTutorial } from "@/browser/contexts/TutorialContext";

interface WorkspaceHeaderProps {
Expand All @@ -24,6 +24,7 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
runtimeConfig,
}) => {
const gitStatus = useGitStatus(workspaceId);
const { canInterrupt } = useWorkspaceSidebarState(workspaceId);
const handleOpenTerminal = useCallback(() => {
void window.api.terminal.openWindow(workspaceId);
}, [workspaceId]);
Expand All @@ -46,13 +47,13 @@ export const WorkspaceHeader: React.FC<WorkspaceHeaderProps> = ({
return (
<div className="bg-separator border-border-light flex h-8 items-center justify-between border-b px-[15px] [@media(max-width:768px)]:h-auto [@media(max-width:768px)]:flex-wrap [@media(max-width:768px)]:gap-2 [@media(max-width:768px)]:py-2 [@media(max-width:768px)]:pl-[60px]">
<div className="text-foreground flex min-w-0 items-center gap-2 overflow-hidden font-semibold">
<WorkspaceStatusDot workspaceId={workspaceId} />
<RuntimeBadge runtimeConfig={runtimeConfig} isWorking={canInterrupt} />
<GitStatusIndicator
gitStatus={gitStatus}
workspaceId={workspaceId}
tooltipPosition="bottom"
isWorking={canInterrupt}
/>
<RuntimeBadge runtimeConfig={runtimeConfig} />
<span className="min-w-0 truncate font-mono text-xs">
{projectName} / {branch}
</span>
Expand Down
29 changes: 9 additions & 20 deletions src/browser/components/WorkspaceListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import { cn } from "@/common/lib/utils";
import { useGitStatus } from "@/browser/stores/GitStatusStore";
import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import React, { useCallback, useState } from "react";
import React, { useState } from "react";
import { GitStatusIndicator } from "./GitStatusIndicator";
import { RuntimeBadge } from "./RuntimeBadge";
import { Tooltip, TooltipWrapper } from "./Tooltip";
import { WorkspaceStatusDot } from "./WorkspaceStatusDot";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
import { Shimmer } from "./ai-elements/shimmer";

Expand All @@ -24,11 +23,13 @@ export interface WorkspaceListItemProps {
projectName: string;
isSelected: boolean;
isDeleting?: boolean;
lastReadTimestamp: number;
/** @deprecated No longer used since status dot was removed, kept for API compatibility */
lastReadTimestamp?: number;
// Event handlers
onSelectWorkspace: (selection: WorkspaceSelection) => void;
onRemoveWorkspace: (workspaceId: string, button: HTMLElement) => Promise<void>;
onToggleUnread: (workspaceId: string) => void;
/** @deprecated No longer used since status dot was removed, kept for API compatibility */
onToggleUnread?: (workspaceId: string) => void;
}

const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
Expand All @@ -37,10 +38,10 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
projectName,
isSelected,
isDeleting,
lastReadTimestamp,
lastReadTimestamp: _lastReadTimestamp,
onSelectWorkspace,
onRemoveWorkspace,
onToggleUnread,
onToggleUnread: _onToggleUnread,
}) => {
// Destructure metadata for convenience
const { id: workspaceId, name: workspaceName, namedWorkspacePath } = metadata;
Expand Down Expand Up @@ -93,12 +94,6 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
}
};

// Memoize toggle unread handler to prevent AgentStatusIndicator re-renders
const handleToggleUnread = useCallback(
() => onToggleUnread(workspaceId),
[onToggleUnread, workspaceId]
);

const { canInterrupt } = useWorkspaceSidebarState(workspaceId);

return (
Expand Down Expand Up @@ -135,16 +130,9 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
data-workspace-path={namedWorkspacePath}
data-workspace-id={workspaceId}
>
<div>
<WorkspaceStatusDot
workspaceId={workspaceId}
lastReadTimestamp={lastReadTimestamp}
onClick={handleToggleUnread}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 items-center gap-1.5">
<RuntimeBadge runtimeConfig={metadata.runtimeConfig} />
<RuntimeBadge runtimeConfig={metadata.runtimeConfig} isWorking={canInterrupt} />
{isEditing ? (
<input
className="bg-input-bg text-input-text border-input-border font-inherit focus:border-input-border-focus -mx-1 min-w-0 flex-1 rounded-sm border px-1 text-left text-[13px] outline-none"
Expand Down Expand Up @@ -181,6 +169,7 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
gitStatus={gitStatus}
workspaceId={workspaceId}
tooltipPosition="right"
isWorking={canInterrupt}
/>

<TooltipWrapper inline>
Expand Down