From e513bb0d1b84473c925e2317d557f0d5e3516f51 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 21 Mar 2026 00:00:57 -0700 Subject: [PATCH] [Haik]: round 3 of dynamic island redesign (better compact view) --- frontend/src/app/components/DynamicIsland.tsx | 225 ++++++++++-------- .../src/app/pages/AgentChat/ApprovalBar.tsx | 8 +- 2 files changed, 130 insertions(+), 103 deletions(-) diff --git a/frontend/src/app/components/DynamicIsland.tsx b/frontend/src/app/components/DynamicIsland.tsx index 10a8183d..21155c48 100644 --- a/frontend/src/app/components/DynamicIsland.tsx +++ b/frontend/src/app/components/DynamicIsland.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react' import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; -import Button from '@mui/material/Button'; import Tooltip from '@mui/material/Tooltip'; import Collapse from '@mui/material/Collapse'; import SearchIcon from '@mui/icons-material/Search'; @@ -24,7 +23,7 @@ import { HistorySession, } from '@/shared/state/agentsSlice'; import { setPendingFocusAgentId } from '@/shared/state/tempStateSlice'; -import ApprovalBar, { BatchApprovalBar } from '@/app/pages/AgentChat/ApprovalBar'; +import ApprovalBar, { BatchApprovalBar, parseMcpToolName, useMcpToolMeta, getToolIcon } from '@/app/pages/AgentChat/ApprovalBar'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; // --------------------------------------------------------------------------- @@ -274,6 +273,16 @@ const DynamicIsland: React.FC = () => { [groups], ); + const oldestNonQuestionApproval = useMemo(() => { + const all = groups + .flatMap((g) => g.approvals) + .filter((a) => a.tool_name !== 'AskUserQuestion'); + if (all.length === 0) return null; + return all.reduce((oldest, a) => + a.created_at < oldest.created_at ? a : oldest, + ); + }, [groups]); + // ---- Island state machine ---- const islandState: IslandState = useMemo(() => { @@ -445,13 +454,14 @@ const DynamicIsland: React.FC = () => { hasApprovals={hasApprovals} /> )} - {islandState === 'compact-actionable' && ( + {islandState === 'compact-actionable' && oldestNonQuestionApproval && ( setUserExpanded(true)} /> )} @@ -577,110 +587,127 @@ const CompactPill: React.FC<{ ); // --------------------------------------------------------------------------- -// Compact-actionable pill — inline approve/deny without expanding +// Compact-actionable pill — single approval with icon + name + approve/deny // --------------------------------------------------------------------------- const CompactActionablePill: React.FC<{ c: ReturnType; - approvalCount: number; - onApproveAll: () => void; - onDenyAll: () => void; + request: ApprovalRequest; + remainingCount: number; + onApprove: (requestId: string) => void; + onDeny: (requestId: string) => void; onExpand: () => void; -}> = ({ c, approvalCount, onApproveAll, onDenyAll, onExpand }) => ( - - = ({ c, request, remainingCount, onApprove, onDeny, onExpand }) => { + const parsed = useMemo(() => parseMcpToolName(request.tool_name), [request.tool_name]); + const meta = useMcpToolMeta(parsed); + + const icon = parsed.isMcp + ? (meta.integration?.icon || null) + : getToolIcon(request.tool_name); + + return ( + - - {approvalCount} {approvalCount === 1 ? 'approval' : 'approvals'} - - - - - { e.stopPropagation(); onExpand(); }} - sx={{ p: 0.25, color: c.text.ghost, '&:hover': { color: c.text.tertiary } }} + - - - - - -); + {icon} + + + {parsed.displayName} + + {remainingCount > 1 && ( + + +{remainingCount - 1} + + )} + + { e.stopPropagation(); onApprove(request.id); }} + sx={{ + p: 0, + width: 18, + height: 18, + color: '#fff', + bgcolor: c.status.success, + '&:hover': { bgcolor: c.status.success, filter: 'brightness(0.85)' }, + }} + > + + + + + { e.stopPropagation(); onDeny(request.id); }} + sx={{ + p: 0, + width: 18, + height: 18, + color: c.status.error, + border: `1px solid ${c.status.error}`, + '&:hover': { bgcolor: `${c.status.error}0a` }, + }} + > + + + + + { e.stopPropagation(); onExpand(); }} + sx={{ p: 0.25, color: c.text.ghost, '&:hover': { color: c.text.tertiary } }} + > + + + + + + ); +}; // --------------------------------------------------------------------------- // Expanded card diff --git a/frontend/src/app/pages/AgentChat/ApprovalBar.tsx b/frontend/src/app/pages/AgentChat/ApprovalBar.tsx index 5cb1ba71..6e703f03 100644 --- a/frontend/src/app/pages/AgentChat/ApprovalBar.tsx +++ b/frontend/src/app/pages/AgentChat/ApprovalBar.tsx @@ -59,14 +59,14 @@ const INTEGRATION_META: Record = { // MCP tool name parser // --------------------------------------------------------------------------- -interface ParsedTool { +export interface ParsedTool { isMcp: boolean; serverSlug: string; actionName: string; displayName: string; } -function parseMcpToolName(rawName: string): ParsedTool { +export function parseMcpToolName(rawName: string): ParsedTool { const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/); if (!m) { return { isMcp: false, serverSlug: '', actionName: rawName, displayName: rawName }; @@ -93,7 +93,7 @@ interface McpToolMeta { serverLabel: string; } -function useMcpToolMeta(parsed: ParsedTool): McpToolMeta { +export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta { const toolItems = useAppSelector((s) => s.tools.items); return useMemo(() => { @@ -185,7 +185,7 @@ interface Props { onDeny: (requestId: string, message?: string) => void; } -function getToolIcon(toolName: string) { +export function getToolIcon(toolName: string) { switch (toolName) { case 'Bash': return ; case 'Read': return ;