Skip to content
Open
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
8 changes: 7 additions & 1 deletion mock/server/public/toolbar/iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,15 @@
// tokens be destroyed and reload the iframe in an unauth state.
log('Has access info', { cookie: Boolean(document.cookie), accessToken: Boolean(accessToken) });

// const csrf = {'X-CSRFToken': getCsrfToken()};

const initWithCreds = {
...init,
headers: { ...init.headers, ...bearer },
headers: {
...init.headers,
...bearer,
...csrf,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undefined variable csrf causes runtime error

High Severity

The variable csrf is referenced in the headers object on line 101 (...csrf), but its declaration on line 94 is commented out. This causes a ReferenceError: csrf is not defined when fetchProxy is invoked, breaking all API calls made through the toolbar's fetch proxy.

Additional Locations (1)

Fix in Cursor Fix in Web

},
credentials: 'same-origin',
};
log({ initWithCreds });
Expand Down
27 changes: 27 additions & 0 deletions src/lib/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import FeatureFlagsPanel from 'toolbar/components/panels/featureFlags/FeatureFla
import FeedbackPanel from 'toolbar/components/panels/feedback/FeedbackPanel';
import IssuesPanel from 'toolbar/components/panels/issues/IssuesPanel';
import NavigationPanel from 'toolbar/components/panels/nav/NavigationPanel';
import SeerExplorerPanel from 'toolbar/components/panels/seerExplorer/SeerExplorerPanel';
import ConfigPanel from 'toolbar/components/panels/settings/ConfigPanel';
import SettingsPanel from 'toolbar/components/panels/settings/SettingsPanel';
import {useApiProxyState} from 'toolbar/context/ApiProxyContext';
import useClearQueryCacheOnProxyStateChange from 'toolbar/hooks/useClearQueryCacheOnProxyStateChange';
import useSeerExplorerAccess from 'toolbar/hooks/useSeerExplorerAccess';

export default function AppRouter() {
useClearQueryCacheOnProxyStateChange();
Expand Down Expand Up @@ -51,6 +53,14 @@ export default function AppRouter() {
}>
<Route path="/issues" element={<IssuesPanel />} />
<Route path="/feedback" element={<FeedbackPanel />} />
<Route
path="/seerExplorer"
element={
<RequireSeerExplorer>
<SeerExplorerPanel />
</RequireSeerExplorer>
}
/>
</Route>
</Route>
</Route>
Expand All @@ -74,3 +84,20 @@ function RequireAuth({children}: {children: ReactNode}) {

return children;
}

function RequireSeerExplorer({children}: {children: ReactNode}) {
const navigate = useNavigate();
const {hasAccess, isPending} = useSeerExplorerAccess();

useEffect(() => {
if (!isPending && !hasAccess) {
navigate('/');
}
}, [hasAccess, isPending, navigate]);

if (isPending || !hasAccess) {
return null;
}

return children;
}
50 changes: 50 additions & 0 deletions src/lib/components/icon/IconSeer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type {SVGIconProps} from 'toolbar/components/icon/SVGIconBase';
import SVGIconBase from 'toolbar/components/icon/SVGIconBase';

interface Props extends SVGIconProps {
animation?: 'loading' | 'waiting';
}

export default function IconSeer({animation, ...props}: Props) {
const commonPath =
'M8.01759 0.25C8.23262 0.249787 8.43757 0.341936 8.58009 0.50293C9.70729 1.77804 11.2269 3.70119 12.626 5.82324C13.9841 7.8832 15.2561 10.1746 15.9317 12.2656C15.9736 12.3357 16.005 12.4135 16.0225 12.4971C16.0949 12.8447 15.9134 13.1959 15.5879 13.3379C13.4024 14.2912 11.151 15 8.01857 15C4.88669 15 2.63318 14.2943 0.451185 13.3457C0.17589 13.226 -0.00176762 12.9535 1.33514e-05 12.6533C0.00080069 12.526 0.0359022 12.4049 0.0947399 12.2979C0.767604 10.203 2.04619 7.9014 3.41115 5.83105C4.81012 3.70913 6.32944 1.78347 7.45607 0.503906L7.51173 0.447266C7.64909 0.321318 7.82933 0.250248 8.01759 0.25ZM13.666 10.6562C12.0686 11.1903 10.117 11.5 8.01857 11.5C5.92124 11.5 3.96583 11.1911 2.37111 10.6572C2.11538 11.1963 1.88727 11.7258 1.69923 12.2402C3.54489 12.9877 5.45222 13.5 8.01857 13.5C10.5835 13.5 12.4883 12.9863 14.336 12.2354C14.1485 11.7218 13.9206 11.1939 13.666 10.6562ZM8.01857 5.5C5.93882 5.50013 4.03972 6.99814 3.14259 9.3291C4.50943 9.74712 6.18916 9.99997 8.01857 10C9.84849 9.99998 11.5258 9.74671 12.8955 9.32812C11.9966 6.998 10.098 5.50012 8.01857 5.5ZM8.01954 2.15234C7.51245 2.75664 6.94957 3.46183 6.37013 4.23438C6.89523 4.08216 7.44681 4.00003 8.01857 4C8.59213 4.00002 9.14526 4.08221 9.67189 4.23535C9.09134 3.46176 8.52751 2.756 8.01954 2.15234Z';

if (animation === 'waiting') {
const waitingClassName = 'pupil-waiting';
return (
<SVGIconBase {...props}>
<style>{`
@keyframes moveHorizontal {
0% { transform: translateX(0); }
5%, 40% { transform: translateX(-1.6px); }
50%, 95% { transform: translateX(1.6px); }
100% { transform: translateX(0); }
}

.${waitingClassName} {
animation: moveHorizontal 4s ease-out infinite;
}
`}</style>
<path d={commonPath} />
<circle className={waitingClassName} cx="8" cy="9" r="2" />
</SVGIconBase>
);
}

if (animation === 'loading') {
return (
<SVGIconBase {...props}>
<path d={commonPath} />
<circle r="2">
<animateMotion path="M 8 7 A 1 1 0 0 1 8 9 A 1 1 0 0 1 8 7" dur="1s" repeatCount="indefinite" />
</circle>
</SVGIconBase>
);
}

return (
<SVGIconBase {...props}>
<path d="M8 0.25C8.21 0.25 8.42 0.34 8.56 0.5C9.69 1.78 11.21 3.7 12.61 5.82C13.97 7.88 15.24 10.17 15.91 12.27C15.96 12.34 15.99 12.41 16 12.5C16.08 12.84 15.89 13.2 15.57 13.34C13.38 14.29 11.13 15 8 15C4.87 15 2.61 14.29 0.43 13.35C0.16 13.23 -0.02 12.95 -0.02 12.65C-0.02 12.53 0.02 12.4 0.08 12.3C0.75 10.2 2.03 7.9 3.39 5.83C4.79 3.71 6.31 1.78 7.44 0.5L7.49 0.45C7.63 0.32 7.81 0.25 8 0.25ZM13.65 10.66C12.05 11.19 10.1 11.5 8 11.5C5.9 11.5 3.95 11.19 2.35 10.66C2.1 11.2 1.87 11.73 1.68 12.24C3.53 12.99 5.43 13.5 8 13.5C10.56 13.5 12.47 12.99 14.32 12.24C14.13 11.72 13.9 11.19 13.65 10.66ZM8 5.5C5.92 5.5 4.02 7 3.12 9.33C4.03 9.61 5.08 9.81 6.22 9.92C6.08 9.64 6 9.33 6 9C6 7.9 6.9 7 8 7C9.1 7 10 7.9 10 9C10 9.33 9.92 9.64 9.78 9.92C10.92 9.81 11.96 9.61 12.88 9.33C11.98 7 10.08 5.5 8 5.5ZM8 2.15C7.49 2.76 6.93 3.46 6.35 4.23C6.88 4.08 7.43 4 8 4C8.57 4 9.13 4.08 9.65 4.24C9.07 3.46 8.51 2.76 8 2.15Z" />
</SVGIconBase>
);
}
35 changes: 22 additions & 13 deletions src/lib/components/panels/nav/NavigationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Indicator from 'toolbar/components/base/Indicator';
import IconFlag from 'toolbar/components/icon/IconFlag';
import IconIssues from 'toolbar/components/icon/IconIssues';
import IconMegaphone from 'toolbar/components/icon/IconMegaphone';
import IconSeer from 'toolbar/components/icon/IconSeer';
import IconSentry from 'toolbar/components/icon/IconSentry';
import NavButton from 'toolbar/components/panels/nav/NavButton';
import NavGrabber from 'toolbar/components/panels/nav/NavGrabber';
Expand All @@ -12,6 +13,7 @@ import {useApiProxyState} from 'toolbar/context/ApiProxyContext';
import {useConfigContext} from 'toolbar/context/ConfigContext';
import {useFeatureFlagAdapterContext} from 'toolbar/context/FeatureFlagAdapterContext';
import {useMousePositionContext} from 'toolbar/context/MousePositionContext';
import useSeerExplorerAccess from 'toolbar/hooks/useSeerExplorerAccess';
import parsePlacement from 'toolbar/utils/parsePlacement';

const navClassName = cva('flex items-center gap-1', {
Expand All @@ -26,14 +28,15 @@ const navClassName = cva('flex items-center gap-1', {
export default function NavigationPanel() {
const [{placement}] = useConfigContext();
const [mousePosition] = useMousePositionContext();
const isMoving = Boolean(mousePosition);
const {isExpanded, setIsHovered} = useNavigationExpansion();

const proxyState = useApiProxyState();
const {overrides} = useFeatureFlagAdapterContext();
const {hasAccess: showSeerExplorer} = useSeerExplorerAccess();

const [major] = parsePlacement(placement);
const isHorizontal = ['top', 'bottom'].includes(major);
const isLoggedIn = proxyState === 'logged-in';

return (
<div
Expand All @@ -44,25 +47,31 @@ export default function NavigationPanel() {
<IconSentry size="sm" />
</NavButton>

<Transition show={isMoving || isExpanded}>
<Transition show={Boolean(mousePosition) || isExpanded}>
<div
className={cx(navClassName({isHorizontal}), 'p-0 transition duration-300 ease-in data-[closed]:opacity-0')}>
<NavGrabber isHorizontal={isHorizontal} />

{proxyState === 'logged-in' ? (
<NavButton to="/issues" tooltip="Issues">
<IconIssues size="sm" />
</NavButton>
) : null}
{isLoggedIn && (
<>
{showSeerExplorer && (
<NavButton to="/seerExplorer" tooltip="Seer Explorer">
<IconSeer size="sm" />
</NavButton>
)}

{proxyState === 'logged-in' ? (
<NavButton to="/feedback" tooltip="User Feedback">
<IconMegaphone size="sm" />
</NavButton>
) : null}
<NavButton to="/issues" tooltip="Issues">
<IconIssues size="sm" />
</NavButton>

<NavButton to="/feedback" tooltip="User Feedback">
<IconMegaphone size="sm" />
</NavButton>
</>
)}

<NavButton to="/featureFlags" tooltip="Feature Flags">
{Object.keys(overrides).length ? <Indicator position="top-right" variant="red" /> : null}
{Object.keys(overrides).length > 0 && <Indicator position="top-right" variant="red" />}
<IconFlag size="sm" />
</NavButton>
</div>
Expand Down
79 changes: 79 additions & 0 deletions src/lib/components/panels/seerExplorer/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import LoadingSpinner from 'toolbar/components/base/LoadingSpinner';
import type {SeerExplorerBlock} from 'toolbar/sentryApi/types/seerExplorer';

interface ChatMessageProps {
block: SeerExplorerBlock;
}

// Validate URL to prevent XSS via javascript: protocol
function isSafeUrl(url: string): boolean {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}

export default function ChatMessage({block}: ChatMessageProps) {
const {message, loading} = block;
const isUser = message.role === 'user';

return (
<div
className={`flex flex-col gap-1 border-b border-b-translucentGray-200 px-2 py-2 ${
isUser ? 'bg-translucentGray-100' : ''
}`}>
<div className="flex flex-row items-center gap-1">
<span className={`text-xs font-medium ${isUser ? 'text-purple-400' : 'text-gray-300'}`}>
{isUser ? 'You' : 'Seer'}
</span>
{loading && <LoadingSpinner size="mini" />}
</div>
<div className="whitespace-pre-wrap text-sm text-gray-300">{message.content}</div>

{/* Show thinking content if available */}
{message.thinking_content && (
<details className="mt-1">
<summary className="cursor-pointer text-xs text-gray-400">Show thinking</summary>
<div className="mt-1 whitespace-pre-wrap text-xs text-gray-400">
{message.thinking_content}
</div>
</details>
)}

{/* Show tool calls if available */}
{message.tool_calls && message.tool_calls.length > 0 && (
<div className="mt-1 flex flex-col gap-0.5">
{message.tool_calls.map(toolCall => (
<div key={toolCall.id} className="text-xs text-gray-400">
🔧 {toolCall.function?.name || toolCall.type}
</div>
))}
</div>
)}

{/* Show tool links if available */}
{block.tool_links && block.tool_links.length > 0 && (
<div className="mt-1 flex flex-col gap-0.5">
{block.tool_links.map((link, idx) =>
isSafeUrl(link.url) ? (
<a
key={idx}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-purple-400 hover:underline">
🔗 {link.title}
</a>
) : (
<span key={idx} className="text-xs text-gray-400">
🔗 {link.title} (invalid URL)
</span>
)
)}
</div>
)}
</div>
);
}
Loading
Loading