Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Latest ui fixes
Browse filesWhat's in here
- put in stop button while loading
- made rejected be different than error
- output of the currently running tool is automatically shown in the righthand console, unless user intentionally cliks and 'locks' a tool, then the view stays on that tool until unlocked
frontend/src/components/Chat/ChatInput.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef, KeyboardEvent } from 'react';
|
|
| 2 |
import { Box, TextField, IconButton, CircularProgress, Typography, Menu, MenuItem, ListItemIcon, ListItemText, Chip } from '@mui/material';
|
| 3 |
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
| 4 |
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
| 5 |
-
import
|
| 6 |
import { apiFetch } from '@/utils/api';
|
| 7 |
|
| 8 |
// Model configuration
|
|
@@ -67,7 +67,6 @@ interface ChatInputProps {
|
|
| 67 |
|
| 68 |
export default function ChatInput({ onSend, onStop, isProcessing = false, disabled = false, placeholder = 'Ask anything...' }: ChatInputProps) {
|
| 69 |
const [input, setInput] = useState('');
|
| 70 |
-
const [stopHovered, setStopHovered] = useState(false);
|
| 71 |
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 72 |
const [selectedModelId, setSelectedModelId] = useState<string>(() => {
|
| 73 |
try {
|
|
@@ -207,20 +206,23 @@ export default function ChatInput({ onSend, onStop, isProcessing = false, disabl
|
|
| 207 |
{isProcessing ? (
|
| 208 |
<IconButton
|
| 209 |
onClick={onStop}
|
| 210 |
-
onMouseEnter={() => setStopHovered(true)}
|
| 211 |
-
onMouseLeave={() => setStopHovered(false)}
|
| 212 |
sx={{
|
| 213 |
mt: 1,
|
| 214 |
-
p: 1,
|
| 215 |
borderRadius: '10px',
|
| 216 |
-
color:
|
| 217 |
transition: 'all 0.2s',
|
|
|
|
| 218 |
'&:hover': {
|
| 219 |
bgcolor: 'var(--hover-bg)',
|
|
|
|
| 220 |
},
|
| 221 |
}}
|
| 222 |
>
|
| 223 |
-
{
|
|
|
|
|
|
|
|
|
|
| 224 |
</IconButton>
|
| 225 |
) : (
|
| 226 |
<IconButton
|
|
|
|
| 2 |
import { Box, TextField, IconButton, CircularProgress, Typography, Menu, MenuItem, ListItemIcon, ListItemText, Chip } from '@mui/material';
|
| 3 |
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
| 4 |
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
| 5 |
+
import StopIcon from '@mui/icons-material/Stop';
|
| 6 |
import { apiFetch } from '@/utils/api';
|
| 7 |
|
| 8 |
// Model configuration
|
|
|
|
| 67 |
|
| 68 |
export default function ChatInput({ onSend, onStop, isProcessing = false, disabled = false, placeholder = 'Ask anything...' }: ChatInputProps) {
|
| 69 |
const [input, setInput] = useState('');
|
|
|
|
| 70 |
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 71 |
const [selectedModelId, setSelectedModelId] = useState<string>(() => {
|
| 72 |
try {
|
|
|
|
| 206 |
{isProcessing ? (
|
| 207 |
<IconButton
|
| 208 |
onClick={onStop}
|
|
|
|
|
|
|
| 209 |
sx={{
|
| 210 |
mt: 1,
|
| 211 |
+
p: 1.5,
|
| 212 |
borderRadius: '10px',
|
| 213 |
+
color: 'var(--muted-text)',
|
| 214 |
transition: 'all 0.2s',
|
| 215 |
+
position: 'relative',
|
| 216 |
'&:hover': {
|
| 217 |
bgcolor: 'var(--hover-bg)',
|
| 218 |
+
color: 'var(--accent-red)',
|
| 219 |
},
|
| 220 |
}}
|
| 221 |
>
|
| 222 |
+
<Box sx={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 223 |
+
<CircularProgress size={28} thickness={3} sx={{ color: 'inherit', position: 'absolute' }} />
|
| 224 |
+
<StopIcon sx={{ fontSize: 16 }} />
|
| 225 |
+
</Box>
|
| 226 |
</IconButton>
|
| 227 |
) : (
|
| 228 |
<IconButton
|
frontend/src/components/Chat/ToolCallGroup.tsx
CHANGED
|
@@ -236,8 +236,8 @@ function costLabel(hardware: string): string | null {
|
|
| 236 |
// Visual helpers
|
| 237 |
// ---------------------------------------------------------------------------
|
| 238 |
|
| 239 |
-
function StatusIcon({ state, cancelled }: { state: ToolPartState; cancelled?: boolean }) {
|
| 240 |
-
if (cancelled) {
|
| 241 |
return <BlockIcon sx={{ fontSize: 16, color: 'var(--muted-text)' }} />;
|
| 242 |
}
|
| 243 |
switch (state) {
|
|
@@ -501,7 +501,7 @@ function InlineApproval({
|
|
| 501 |
// ---------------------------------------------------------------------------
|
| 502 |
|
| 503 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 504 |
-
const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, setToolError, getToolError } = useAgentStore();
|
| 505 |
const researchSteps = useAgentStore(s => {
|
| 506 |
const activeId = s.activeSessionId;
|
| 507 |
return activeId ? (s.sessionStates[activeId]?.researchSteps) : undefined;
|
|
@@ -527,6 +527,9 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 527 |
// Track which toolCallIds we've already submitted so we can detect new approval rounds
|
| 528 |
const submittedIdsRef = useRef<Set<string>>(new Set());
|
| 529 |
|
|
|
|
|
|
|
|
|
|
| 530 |
// Reset submission state when new (unseen) pending tools arrive β e.g. second approval round
|
| 531 |
useEffect(() => {
|
| 532 |
if (!isSubmitting || pendingTools.length === 0) return;
|
|
@@ -602,6 +605,10 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 602 |
if (editedScript) {
|
| 603 |
logger.log(`Sending edited script for ${toolCallId} (${editedScript.length} chars)`);
|
| 604 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
return {
|
| 606 |
tool_call_id: toolCallId,
|
| 607 |
approved: d.approved,
|
|
@@ -621,7 +628,7 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 621 |
setIsSubmitting(false);
|
| 622 |
}
|
| 623 |
},
|
| 624 |
-
[approveTools, lockPanel, getEditedScript],
|
| 625 |
);
|
| 626 |
|
| 627 |
const handleApproveAll = useCallback(() => {
|
|
@@ -657,8 +664,8 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 657 |
});
|
| 658 |
}, []);
|
| 659 |
|
| 660 |
-
// ββ
|
| 661 |
-
const
|
| 662 |
(tool: DynamicToolPart) => {
|
| 663 |
const args = tool.input as Record<string, unknown> | undefined;
|
| 664 |
const displayName = toolDisplayMap[tool.toolCallId] || tool.toolName;
|
|
@@ -684,7 +691,13 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 684 |
const inputSection = args ? { content: JSON.stringify(args, null, 2), language: 'json' } : undefined;
|
| 685 |
|
| 686 |
const outputText = tool.output ?? (tool.state === 'output-error' ? (tool as Record<string, unknown>).errorText : undefined);
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
let language = 'text';
|
| 689 |
const content = String(outputText);
|
| 690 |
if (content.trim().startsWith('{') || content.trim().startsWith('[')) language = 'json';
|
|
@@ -696,6 +709,15 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 696 |
const content = `Tool \`${tool.toolName}\` returned an error with no output message.`;
|
| 697 |
setPanel({ title: displayName, output: { content, language: 'markdown' }, input: inputSection }, 'output');
|
| 698 |
setRightPanelOpen(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
} else if (args) {
|
| 700 |
const runningMessages = [
|
| 701 |
'Crunching numbers and herding tensors...',
|
|
@@ -715,6 +737,51 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 715 |
[toolDisplayMap, setPanel, getEditedScript, setRightPanelOpen, setLeftSidebarOpen],
|
| 716 |
);
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
// ββ Parse hf_jobs metadata from output ββββββββββββββββββββββββββββ
|
| 719 |
function parseJobMeta(output: unknown): { jobUrl?: string; jobStatus?: string } {
|
| 720 |
if (typeof output !== 'string') return {};
|
|
@@ -808,7 +875,7 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 808 |
const cancelled = isCancelledTool(tool);
|
| 809 |
const currentlyHasError = state === 'output-error';
|
| 810 |
const persistedError = getToolError(tool.toolCallId);
|
| 811 |
-
const
|
| 812 |
|
| 813 |
// Stale in-progress tools after page reload: treat as completed
|
| 814 |
const stale = !isProcessing && (state === 'input-available' || state === 'input-streaming');
|
|
@@ -816,7 +883,10 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 816 |
: isPending && localDecision
|
| 817 |
? (localDecision.approved ? 'input-available' : 'output-denied')
|
| 818 |
: state;
|
|
|
|
|
|
|
| 819 |
const label = cancelled ? 'cancelled'
|
|
|
|
| 820 |
: hasError ? 'error'
|
| 821 |
: statusLabel(displayState as ToolPartState);
|
| 822 |
|
|
@@ -853,11 +923,14 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 853 |
py: 1,
|
| 854 |
cursor: isPending ? 'default' : clickable ? 'pointer' : 'default',
|
| 855 |
transition: 'background-color 0.15s',
|
|
|
|
|
|
|
| 856 |
'&:hover': clickable && !isPending ? { bgcolor: 'var(--hover-bg)' } : {},
|
| 857 |
}}
|
| 858 |
>
|
| 859 |
<StatusIcon
|
| 860 |
cancelled={cancelled}
|
|
|
|
| 861 |
state={
|
| 862 |
hasError
|
| 863 |
? 'output-error'
|
|
@@ -895,6 +968,7 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 895 |
: null;
|
| 896 |
const chipLabel = researchLabel || label;
|
| 897 |
if (!chipLabel || (tool.toolName === 'hf_jobs' && jobMeta.jobStatus)) return null;
|
|
|
|
| 898 |
return (
|
| 899 |
<Chip
|
| 900 |
label={chipLabel}
|
|
@@ -903,12 +977,11 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 903 |
height: 20,
|
| 904 |
fontSize: '0.65rem',
|
| 905 |
fontWeight: 600,
|
| 906 |
-
bgcolor: cancelled ? 'rgba(255,255,255,0.05)'
|
| 907 |
: hasError ? 'rgba(224,90,79,0.12)'
|
| 908 |
-
: displayState === 'output-denied' ? 'rgba(255,255,255,0.05)'
|
| 909 |
: (researchLabel && displayState === 'output-available') ? 'rgba(47,204,113,0.12)'
|
| 910 |
: 'var(--accent-yellow-weak)',
|
| 911 |
-
color: cancelled ? 'var(--muted-text)'
|
| 912 |
: hasError ? 'var(--accent-red)'
|
| 913 |
: statusColor(displayState as ToolPartState),
|
| 914 |
letterSpacing: '0.03em',
|
|
|
|
| 236 |
// Visual helpers
|
| 237 |
// ---------------------------------------------------------------------------
|
| 238 |
|
| 239 |
+
function StatusIcon({ state, cancelled, isRejected }: { state: ToolPartState; cancelled?: boolean; isRejected?: boolean }) {
|
| 240 |
+
if (cancelled || isRejected) {
|
| 241 |
return <BlockIcon sx={{ fontSize: 16, color: 'var(--muted-text)' }} />;
|
| 242 |
}
|
| 243 |
switch (state) {
|
|
|
|
| 501 |
// ---------------------------------------------------------------------------
|
| 502 |
|
| 503 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 504 |
+
const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, setToolError, getToolError, setToolRejected, getToolRejected } = useAgentStore();
|
| 505 |
const researchSteps = useAgentStore(s => {
|
| 506 |
const activeId = s.activeSessionId;
|
| 507 |
return activeId ? (s.sessionStates[activeId]?.researchSteps) : undefined;
|
|
|
|
| 527 |
// Track which toolCallIds we've already submitted so we can detect new approval rounds
|
| 528 |
const submittedIdsRef = useRef<Set<string>>(new Set());
|
| 529 |
|
| 530 |
+
// ββ Panel lock state (for auto-follow vs user-selected) βββββββββββ
|
| 531 |
+
const [lockedToolId, setLockedToolId] = useState<string | null>(null);
|
| 532 |
+
|
| 533 |
// Reset submission state when new (unseen) pending tools arrive β e.g. second approval round
|
| 534 |
useEffect(() => {
|
| 535 |
if (!isSubmitting || pendingTools.length === 0) return;
|
|
|
|
| 605 |
if (editedScript) {
|
| 606 |
logger.log(`Sending edited script for ${toolCallId} (${editedScript.length} chars)`);
|
| 607 |
}
|
| 608 |
+
// Mark tool as rejected if not approved
|
| 609 |
+
if (!d.approved) {
|
| 610 |
+
setToolRejected(toolCallId, true);
|
| 611 |
+
}
|
| 612 |
return {
|
| 613 |
tool_call_id: toolCallId,
|
| 614 |
approved: d.approved,
|
|
|
|
| 628 |
setIsSubmitting(false);
|
| 629 |
}
|
| 630 |
},
|
| 631 |
+
[approveTools, lockPanel, getEditedScript, setToolRejected],
|
| 632 |
);
|
| 633 |
|
| 634 |
const handleApproveAll = useCallback(() => {
|
|
|
|
| 664 |
});
|
| 665 |
}, []);
|
| 666 |
|
| 667 |
+
// ββ Show tool panel (shared logic) ββββββββββββββββββββββββββββββββ
|
| 668 |
+
const showToolPanel = useCallback(
|
| 669 |
(tool: DynamicToolPart) => {
|
| 670 |
const args = tool.input as Record<string, unknown> | undefined;
|
| 671 |
const displayName = toolDisplayMap[tool.toolCallId] || tool.toolName;
|
|
|
|
| 691 |
const inputSection = args ? { content: JSON.stringify(args, null, 2), language: 'json' } : undefined;
|
| 692 |
|
| 693 |
const outputText = tool.output ?? (tool.state === 'output-error' ? (tool as Record<string, unknown>).errorText : undefined);
|
| 694 |
+
|
| 695 |
+
// Determine if tool is still running or has completed
|
| 696 |
+
const isRunning = tool.state === 'input-available' || tool.state === 'input-streaming' || tool.state === 'approval-responded';
|
| 697 |
+
const hasCompleted = tool.state === 'output-available' || tool.state === 'output-error' || tool.state === 'output-denied';
|
| 698 |
+
|
| 699 |
+
if (outputText) {
|
| 700 |
+
// Tool has output - show it (regardless of state)
|
| 701 |
let language = 'text';
|
| 702 |
const content = String(outputText);
|
| 703 |
if (content.trim().startsWith('{') || content.trim().startsWith('[')) language = 'json';
|
|
|
|
| 709 |
const content = `Tool \`${tool.toolName}\` returned an error with no output message.`;
|
| 710 |
setPanel({ title: displayName, output: { content, language: 'markdown' }, input: inputSection }, 'output');
|
| 711 |
setRightPanelOpen(true);
|
| 712 |
+
} else if (hasCompleted && args) {
|
| 713 |
+
// Tool completed but has no output - show input as fallback
|
| 714 |
+
setPanel({ title: displayName, output: { content: JSON.stringify(args, null, 2), language: 'json' }, input: inputSection }, 'output');
|
| 715 |
+
setRightPanelOpen(true);
|
| 716 |
+
} else if (isRunning && args) {
|
| 717 |
+
// Tool is still running - show running message
|
| 718 |
+
const content = `Tool \`${tool.toolName}\` is still running...\n\nClick the input tab to view the tool arguments.`;
|
| 719 |
+
setPanel({ title: displayName, output: { content, language: 'markdown' }, input: inputSection }, 'output');
|
| 720 |
+
setRightPanelOpen(true);
|
| 721 |
} else if (args) {
|
| 722 |
const runningMessages = [
|
| 723 |
'Crunching numbers and herding tensors...',
|
|
|
|
| 737 |
[toolDisplayMap, setPanel, getEditedScript, setRightPanelOpen, setLeftSidebarOpen],
|
| 738 |
);
|
| 739 |
|
| 740 |
+
// ββ Panel click handler βββββββββββββββββββββββββββββββββββββββββββ
|
| 741 |
+
const handleClick = useCallback(
|
| 742 |
+
(tool: DynamicToolPart) => {
|
| 743 |
+
// Toggle lock: if clicking the same tool that's already locked, unlock it
|
| 744 |
+
if (lockedToolId === tool.toolCallId) {
|
| 745 |
+
setLockedToolId(null);
|
| 746 |
+
return;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
// Lock this tool
|
| 750 |
+
setLockedToolId(tool.toolCallId);
|
| 751 |
+
|
| 752 |
+
// Show the panel
|
| 753 |
+
showToolPanel(tool);
|
| 754 |
+
},
|
| 755 |
+
[lockedToolId, showToolPanel],
|
| 756 |
+
);
|
| 757 |
+
|
| 758 |
+
// ββ Auto-follow currently active tool when not locked βββββββββββββ
|
| 759 |
+
const activeToolIdRef = useRef<string | null>(null);
|
| 760 |
+
|
| 761 |
+
useEffect(() => {
|
| 762 |
+
if (lockedToolId !== null) return; // User has locked a tool, don't auto-follow
|
| 763 |
+
|
| 764 |
+
// Find the currently running tool (latest tool that's in progress)
|
| 765 |
+
const runningTool = tools.slice().reverse().find(t =>
|
| 766 |
+
t.state === 'input-available' ||
|
| 767 |
+
t.state === 'input-streaming' ||
|
| 768 |
+
t.state === 'approval-responded'
|
| 769 |
+
);
|
| 770 |
+
|
| 771 |
+
if (runningTool) {
|
| 772 |
+
// Track this as the active tool and show its panel
|
| 773 |
+
activeToolIdRef.current = runningTool.toolCallId;
|
| 774 |
+
showToolPanel(runningTool);
|
| 775 |
+
} else if (activeToolIdRef.current) {
|
| 776 |
+
// No running tool, but we were following one - check if it completed
|
| 777 |
+
const completedTool = tools.find(t => t.toolCallId === activeToolIdRef.current);
|
| 778 |
+
if (completedTool && (completedTool.state === 'output-available' || completedTool.state === 'output-error')) {
|
| 779 |
+
// The tool we were following has completed - update its panel
|
| 780 |
+
showToolPanel(completedTool);
|
| 781 |
+
}
|
| 782 |
+
}
|
| 783 |
+
}, [tools, lockedToolId, showToolPanel]);
|
| 784 |
+
|
| 785 |
// ββ Parse hf_jobs metadata from output ββββββββββββββββββββββββββββ
|
| 786 |
function parseJobMeta(output: unknown): { jobUrl?: string; jobStatus?: string } {
|
| 787 |
if (typeof output !== 'string') return {};
|
|
|
|
| 875 |
const cancelled = isCancelledTool(tool);
|
| 876 |
const currentlyHasError = state === 'output-error';
|
| 877 |
const persistedError = getToolError(tool.toolCallId);
|
| 878 |
+
const persistedRejection = getToolRejected(tool.toolCallId);
|
| 879 |
|
| 880 |
// Stale in-progress tools after page reload: treat as completed
|
| 881 |
const stale = !isProcessing && (state === 'input-available' || state === 'input-streaming');
|
|
|
|
| 883 |
: isPending && localDecision
|
| 884 |
? (localDecision.approved ? 'input-available' : 'output-denied')
|
| 885 |
: state;
|
| 886 |
+
const isRejected = displayState === 'output-denied' || persistedRejection;
|
| 887 |
+
const hasError = (persistedError || currentlyHasError) && !isRejected;
|
| 888 |
const label = cancelled ? 'cancelled'
|
| 889 |
+
: isRejected ? 'rejected'
|
| 890 |
: hasError ? 'error'
|
| 891 |
: statusLabel(displayState as ToolPartState);
|
| 892 |
|
|
|
|
| 923 |
py: 1,
|
| 924 |
cursor: isPending ? 'default' : clickable ? 'pointer' : 'default',
|
| 925 |
transition: 'background-color 0.15s',
|
| 926 |
+
bgcolor: lockedToolId === tool.toolCallId ? 'var(--hover-bg)' : 'transparent',
|
| 927 |
+
borderLeft: lockedToolId === tool.toolCallId ? '3px solid var(--accent-yellow)' : '3px solid transparent',
|
| 928 |
'&:hover': clickable && !isPending ? { bgcolor: 'var(--hover-bg)' } : {},
|
| 929 |
}}
|
| 930 |
>
|
| 931 |
<StatusIcon
|
| 932 |
cancelled={cancelled}
|
| 933 |
+
isRejected={isRejected}
|
| 934 |
state={
|
| 935 |
hasError
|
| 936 |
? 'output-error'
|
|
|
|
| 968 |
: null;
|
| 969 |
const chipLabel = researchLabel || label;
|
| 970 |
if (!chipLabel || (tool.toolName === 'hf_jobs' && jobMeta.jobStatus)) return null;
|
| 971 |
+
|
| 972 |
return (
|
| 973 |
<Chip
|
| 974 |
label={chipLabel}
|
|
|
|
| 977 |
height: 20,
|
| 978 |
fontSize: '0.65rem',
|
| 979 |
fontWeight: 600,
|
| 980 |
+
bgcolor: (cancelled || isRejected) ? 'rgba(255,255,255,0.05)'
|
| 981 |
: hasError ? 'rgba(224,90,79,0.12)'
|
|
|
|
| 982 |
: (researchLabel && displayState === 'output-available') ? 'rgba(47,204,113,0.12)'
|
| 983 |
: 'var(--accent-yellow-weak)',
|
| 984 |
+
color: (cancelled || isRejected) ? 'var(--muted-text)'
|
| 985 |
: hasError ? 'var(--accent-red)'
|
| 986 |
: statusColor(displayState as ToolPartState),
|
| 987 |
letterSpacing: '0.03em',
|
frontend/src/store/agentStore.ts
CHANGED
|
@@ -111,6 +111,9 @@ interface AgentStore {
|
|
| 111 |
// Tool error states (tool_call_id -> true if errored) - persisted across renders
|
| 112 |
toolErrors: Record<string, boolean>;
|
| 113 |
|
|
|
|
|
|
|
|
|
|
| 114 |
// ββ Per-session actions βββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
|
| 116 |
/** Update a session's state. If it's the active session, also update flat state. */
|
|
@@ -154,6 +157,9 @@ interface AgentStore {
|
|
| 154 |
|
| 155 |
setToolError: (toolCallId: string, hasError: boolean) => void;
|
| 156 |
getToolError: (toolCallId: string) => boolean | undefined;
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
/**
|
|
@@ -194,6 +200,25 @@ function saveToolErrors(errors: Record<string, boolean>): void {
|
|
| 194 |
}
|
| 195 |
}
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
export const useAgentStore = create<AgentStore>()((set, get) => ({
|
| 198 |
sessionStates: {},
|
| 199 |
activeSessionId: null,
|
|
@@ -215,6 +240,7 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
|
|
| 215 |
jobUrls: {},
|
| 216 |
jobStatuses: {},
|
| 217 |
toolErrors: loadToolErrors(),
|
|
|
|
| 218 |
|
| 219 |
// ββ Per-session state management ββββββββββββββββββββββββββββββββββ
|
| 220 |
|
|
@@ -409,4 +435,16 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
|
|
| 409 |
},
|
| 410 |
|
| 411 |
getToolError: (toolCallId) => get().toolErrors[toolCallId],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
}));
|
|
|
|
| 111 |
// Tool error states (tool_call_id -> true if errored) - persisted across renders
|
| 112 |
toolErrors: Record<string, boolean>;
|
| 113 |
|
| 114 |
+
// Tool rejected states (tool_call_id -> true if rejected by user) - persisted across renders
|
| 115 |
+
rejectedTools: Record<string, boolean>;
|
| 116 |
+
|
| 117 |
// ββ Per-session actions βββββββββββββββββββββββββββββββββββββββββββββ
|
| 118 |
|
| 119 |
/** Update a session's state. If it's the active session, also update flat state. */
|
|
|
|
| 157 |
|
| 158 |
setToolError: (toolCallId: string, hasError: boolean) => void;
|
| 159 |
getToolError: (toolCallId: string) => boolean | undefined;
|
| 160 |
+
|
| 161 |
+
setToolRejected: (toolCallId: string, isRejected: boolean) => void;
|
| 162 |
+
getToolRejected: (toolCallId: string) => boolean | undefined;
|
| 163 |
}
|
| 164 |
|
| 165 |
/**
|
|
|
|
| 200 |
}
|
| 201 |
}
|
| 202 |
|
| 203 |
+
// Load persisted rejected tools from localStorage
|
| 204 |
+
function loadRejectedTools(): Record<string, boolean> {
|
| 205 |
+
try {
|
| 206 |
+
const stored = localStorage.getItem('hf-agent-rejected-tools');
|
| 207 |
+
return stored ? JSON.parse(stored) : {};
|
| 208 |
+
} catch {
|
| 209 |
+
return {};
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// Save rejected tools to localStorage
|
| 214 |
+
function saveRejectedTools(rejected: Record<string, boolean>): void {
|
| 215 |
+
try {
|
| 216 |
+
localStorage.setItem('hf-agent-rejected-tools', JSON.stringify(rejected));
|
| 217 |
+
} catch (e) {
|
| 218 |
+
console.warn('Failed to persist rejected tools:', e);
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
export const useAgentStore = create<AgentStore>()((set, get) => ({
|
| 223 |
sessionStates: {},
|
| 224 |
activeSessionId: null,
|
|
|
|
| 240 |
jobUrls: {},
|
| 241 |
jobStatuses: {},
|
| 242 |
toolErrors: loadToolErrors(),
|
| 243 |
+
rejectedTools: loadRejectedTools(),
|
| 244 |
|
| 245 |
// ββ Per-session state management ββββββββββββββββββββββββββββββββββ
|
| 246 |
|
|
|
|
| 435 |
},
|
| 436 |
|
| 437 |
getToolError: (toolCallId) => get().toolErrors[toolCallId],
|
| 438 |
+
|
| 439 |
+
// ββ Tool Rejections ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 440 |
+
|
| 441 |
+
setToolRejected: (toolCallId, isRejected) => {
|
| 442 |
+
set((state) => {
|
| 443 |
+
const updated = { ...state.rejectedTools, [toolCallId]: isRejected };
|
| 444 |
+
saveRejectedTools(updated);
|
| 445 |
+
return { rejectedTools: updated };
|
| 446 |
+
});
|
| 447 |
+
},
|
| 448 |
+
|
| 449 |
+
getToolRejected: (toolCallId) => get().rejectedTools[toolCallId],
|
| 450 |
}));
|