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
52 changes: 8 additions & 44 deletions src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,8 @@ import { useAutoScroll } from "@/hooks/useAutoScroll";
import { usePersistedState } from "@/hooks/usePersistedState";
import { useThinking } from "@/contexts/ThinkingContext";
import { useWorkspaceState, useWorkspaceAggregator } from "@/stores/WorkspaceStore";
import { StatusIndicator } from "./StatusIndicator";
import { WorkspaceHeader } from "./WorkspaceHeader";
import { getModelName } from "@/utils/ai/models";
import { GitStatusIndicator } from "./GitStatusIndicator";
import { RuntimeBadge } from "./RuntimeBadge";

import { useGitStatus } from "@/stores/GitStatusStore";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import type { DisplayedMessage } from "@/types/message";
import type { RuntimeConfig } from "@/types/runtime";
import { useAIViewKeybinds } from "@/hooks/useAIViewKeybinds";
Expand Down Expand Up @@ -75,9 +70,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
const workspaceState = useWorkspaceState(workspaceId);
const aggregator = useWorkspaceAggregator(workspaceId);

// Get git status for this workspace
const gitStatus = useGitStatus(workspaceId);

const [editingMessage, setEditingMessage] = useState<{ id: string; content: string } | undefined>(
undefined
);
Expand Down Expand Up @@ -339,41 +331,13 @@ const AIViewInner: React.FC<AIViewProps> = ({
ref={chatAreaRef}
className="flex min-w-96 flex-1 flex-col [@media(max-width:768px)]:max-h-full [@media(max-width:768px)]:w-full [@media(max-width:768px)]:min-w-0"
>
<div className="bg-separator border-border-light flex items-center justify-between border-b px-[15px] py-1 [@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">
<StatusIndicator
streaming={canInterrupt}
title={
canInterrupt && currentModel ? `${getModelName(currentModel)} streaming` : "Idle"
}
/>
<GitStatusIndicator
gitStatus={gitStatus}
workspaceId={workspaceId}
tooltipPosition="bottom"
/>
<RuntimeBadge runtimeConfig={runtimeConfig} />
<span className="min-w-0 truncate font-mono text-xs">
{projectName} / {branch}
</span>
<span className="text-muted min-w-0 truncate font-mono text-[11px] font-normal">
{namedWorkspacePath}
</span>
<TooltipWrapper inline>
<button
onClick={handleOpenTerminal}
className="text-muted hover:text-foreground flex cursor-pointer items-center justify-center border-none bg-transparent p-1 transition-colors [&_svg]:h-4 [&_svg]:w-4"
>
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 01-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z" />
</svg>
</button>
<Tooltip className="tooltip" position="bottom" align="center">
Open in terminal ({formatKeybind(KEYBINDS.OPEN_TERMINAL)})
</Tooltip>
</TooltipWrapper>
</div>
</div>
<WorkspaceHeader
workspaceId={workspaceId}
projectName={projectName}
branch={branch}
namedWorkspacePath={namedWorkspacePath}
runtimeConfig={runtimeConfig}
/>

<div className="relative flex-1 overflow-hidden">
<div
Expand Down
111 changes: 111 additions & 0 deletions src/components/AgentStatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useCallback, useMemo } from "react";
import { cn } from "@/lib/utils";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import { useWorkspaceSidebarState } from "@/stores/WorkspaceStore";
import { getStatusTooltip } from "@/utils/ui/statusTooltip";

interface AgentStatusIndicatorProps {
workspaceId: string;
// Sidebar-specific props (optional)
lastReadTimestamp?: number;
onClick?: (e: React.MouseEvent) => void;
// Display props
size?: number;
className?: string;
}

export const AgentStatusIndicator: React.FC<AgentStatusIndicatorProps> = ({
workspaceId,
lastReadTimestamp,
onClick,
size = 8,
className,
}) => {
// Get workspace state
const { canInterrupt, currentModel, agentStatus, recencyTimestamp } =
useWorkspaceSidebarState(workspaceId);

const streaming = canInterrupt;

// Compute unread status if lastReadTimestamp provided (sidebar only)
const unread = useMemo(() => {
if (lastReadTimestamp === undefined) return false;
return recencyTimestamp !== null && recencyTimestamp > lastReadTimestamp;
}, [lastReadTimestamp, recencyTimestamp]);

// Compute tooltip
const title = useMemo(
() =>
getStatusTooltip({
isStreaming: streaming,
streamingModel: currentModel,
agentStatus,
isUnread: unread,
recencyTimestamp,
}),
[streaming, currentModel, agentStatus, unread, recencyTimestamp]
);

const handleClick = useCallback(
(e: React.MouseEvent) => {
// Only allow clicking when not streaming
if (!streaming && onClick) {
e.stopPropagation(); // Prevent workspace selection
onClick(e);
}
},
[streaming, onClick]
);

const bgColor = streaming ? "bg-assistant-border" : unread ? "bg-white" : "bg-muted-dark";
const cursor = onClick && !streaming ? "cursor-pointer" : "cursor-default";

// Always show dot, add emoji next to it when available
const dot = (
<div
style={{ width: size, height: size }}
className={cn(
"rounded-full shrink-0 transition-colors duration-200",
bgColor,
cursor,
onClick && !streaming && "hover:opacity-70"
)}
onClick={handleClick}
/>
);

const emoji = agentStatus ? (
<div
className="flex shrink-0 items-center justify-center transition-all duration-200"
style={{
fontSize: size * 1.5,
filter: streaming ? "none" : "grayscale(100%)",
opacity: streaming ? 1 : 0.6,
}}
>
{agentStatus.emoji}
</div>
) : null;

// Container holds both emoji and dot (emoji on left)
const indicator = (
<div className={cn("flex items-center gap-1.5", className)} onClick={handleClick}>
{emoji}
{dot}
</div>
);

// If tooltip content provided, wrap with proper Tooltip component
if (title) {
return (
<TooltipWrapper inline>
{indicator}
<Tooltip className="tooltip" align="center">
{title}
</Tooltip>
</TooltipWrapper>
);
}

return indicator;
};
20 changes: 20 additions & 0 deletions src/components/Messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FileEditToolCall } from "../tools/FileEditToolCall";
import { FileReadToolCall } from "../tools/FileReadToolCall";
import { ProposePlanToolCall } from "../tools/ProposePlanToolCall";
import { TodoToolCall } from "../tools/TodoToolCall";
import { StatusSetToolCall } from "../tools/StatusSetToolCall";
import type {
BashToolArgs,
BashToolResult,
Expand All @@ -22,6 +23,8 @@ import type {
ProposePlanToolResult,
TodoWriteToolArgs,
TodoWriteToolResult,
StatusSetToolArgs,
StatusSetToolResult,
} from "@/types/tools";

interface ToolMessageProps {
Expand Down Expand Up @@ -73,6 +76,11 @@ function isTodoWriteTool(toolName: string, args: unknown): args is TodoWriteTool
return TOOL_DEFINITIONS.todo_write.schema.safeParse(args).success;
}

function isStatusSetTool(toolName: string, args: unknown): args is StatusSetToolArgs {
if (toolName !== "status_set") return false;
return TOOL_DEFINITIONS.status_set.schema.safeParse(args).success;
}

export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, workspaceId }) => {
// Route to specialized components based on tool name
if (isBashTool(message.toolName, message.args)) {
Expand Down Expand Up @@ -164,6 +172,18 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, wo
);
}

if (isStatusSetTool(message.toolName, message.args)) {
return (
<div className={className}>
<StatusSetToolCall
args={message.args}
result={message.result as StatusSetToolResult | undefined}
status={message.status}
/>
</div>
);
}

// Fallback to generic tool call
return (
<div className={className}>
Expand Down
Loading