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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.324"
VERSION = "0.261.325"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
4 changes: 3 additions & 1 deletion application/v2_ui/src/components/chat/ConversationBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ const TONE_TITLE: Record<WorkspaceBadgeTone, string> = {

export function ConversationBadges({
metadata,
className,
}: {
metadata: ConversationMetadata | null | undefined;
className?: string;
}) {
const classificationEnabled = useBootstrapStore((state) =>
Boolean(state.data?.features?.enable_document_classification),
Expand All @@ -53,7 +55,7 @@ export function ConversationBadges({
}

return (
<div className="flex shrink-0 items-center gap-1.5">
<div className={clsx('flex shrink-0 items-center gap-1.5', className)}>
{lock !== 'hidden' && (
<span
title={
Expand Down
3 changes: 2 additions & 1 deletion application/v2_ui/src/components/layout/CategoryRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ReactNode } from 'react';
import clsx from 'clsx';
import { PanelLeftClose, PanelLeftOpen, type LucideIcon } from 'lucide-react';
import { PageHeader } from './PageHeader';
import type { HeaderActionItems } from './HeaderActions';

export interface RailItem {
id: string;
Expand Down Expand Up @@ -61,7 +62,7 @@ export function CategoryRailPage({
testId: string;
title: string;
description: string;
actions?: ReactNode;
actions?: HeaderActionItems;
/** The rail's accessible name, such as "Approval categories". */
railLabel: string;
railTestId: string;
Expand Down
84 changes: 84 additions & 0 deletions application/v2_ui/src/components/layout/HeaderActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// HeaderActions.tsx
// Only explicitly visible actions count toward mobile overflow; statuses and dialog hosts do not.

import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react';
import { ChevronDown, Loader2 } from 'lucide-react';
import { clsx } from 'clsx';
import { GlassButton } from '../ui/primitives';
import { ActionPopover } from '../ui/ActionPopover';
import { useMobileViewport } from '../../lib/useMobileViewport';

export interface HeaderAction {
id: string;
label: string;
icon: ReactNode;
onClick: () => void;
variant?: 'primary' | 'subtle' | 'ghost';
disabled?: boolean;

Check warning on line 17 in application/v2_ui/src/components/layout/HeaderActions.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
loading?: boolean;
/** Tours already use an icon on desktop. */
iconOnly?: boolean;
testId?: string;
}

export type HeaderActionItems = readonly (HeaderAction | null | false | undefined)[];

export function HeaderActionControl({ action, inPopover = false, onActivate }: {
action: HeaderAction;
inPopover?: boolean;
onActivate?: () => void;
}) {
return (
<GlassButton type="button" size="sm" variant={action.variant}
disabled={action.disabled || action.loading} aria-busy={action.loading || undefined}

Check warning on line 33 in application/v2_ui/src/components/layout/HeaderActions.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
aria-label={action.label} title={action.label} data-testid={action.testId}
onClick={() => {
onActivate?.();
action.onClick();
}}
className={clsx('header-action shrink-0', inPopover && 'w-full justify-start',
action.iconOnly && !inPopover && 'header-action-icon-only')}>
<span aria-hidden="true" className="flex shrink-0 items-center">

Check warning on line 41 in application/v2_ui/src/components/layout/HeaderActions.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
{action.loading ? <Loader2 size={14} className="animate-spin" /> : action.icon}
</span>
<span className={clsx(!inPopover && 'header-action-label')}>{action.label}</span>
</GlassButton>
);
}

export function HeaderActions({ actions }: { actions: HeaderActionItems }) {
const mobile = useMobileViewport();
const visible = actions.filter((action): action is HeaderAction => Boolean(action));
const overflow = mobile && visible.length > 2;
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const id = useId();
const close = useCallback((returnFocus: boolean) => {
setOpen(false);
if (returnFocus) triggerRef.current?.focus();
}, []);
const actionIds = visible.map((action) => action.id).join('|');
useEffect(() => setOpen(false), [mobile, actionIds]);

if (!visible.length) return null;
return (
<div className="page-header-actions ml-auto flex max-w-full shrink-0 items-center gap-2 md:flex-wrap">
{(overflow ? visible.slice(0, 1) : visible).map((action) =>
<HeaderActionControl key={action.id} action={action} />)}
{overflow && (
<button ref={triggerRef} type="button" title="More actions" aria-label="More actions"
aria-expanded={open} aria-controls={id}
onClick={() => setOpen((value) => !value)}
className="header-action flex shrink-0 items-center justify-center rounded-xl text-text-2 hover:bg-surface-2">
<ChevronDown size={18} aria-hidden="true" />

Check warning on line 73 in application/v2_ui/src/components/layout/HeaderActions.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
</button>
)}
{overflow && open && (
<ActionPopover id={id} label="More actions" anchorRef={triggerRef} onClose={close}>
{visible.slice(1).map((action) =>
<HeaderActionControl key={action.id} action={action} inPopover onActivate={() => close(true)} />)}
</ActionPopover>
)}
</div>
);
}
12 changes: 8 additions & 4 deletions application/v2_ui/src/components/layout/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,34 @@

import type { ReactNode } from 'react';
import { MobileNavigation } from './MobileNavigation';
import { HeaderActions, type HeaderActionItems } from './HeaderActions';

export function PageHeader({
title,
description,
actions,
leading,
status,
}: {
title: string;
description?: string;
actions?: ReactNode;
actions?: HeaderActionItems;
status?: ReactNode;
/** Rendered before the title block, for a page identified by more than its name. */
leading?: ReactNode;
}) {
return (
<header className="page-header glass glass-edge flex min-h-14 shrink-0 flex-wrap items-center gap-2 rounded-none border-t-0 border-r-0 px-2 py-2 md:gap-4 md:px-5 md:py-3">
<header className="page-header glass glass-edge flex min-h-14 shrink-0 items-center gap-2 rounded-none border-t-0 border-r-0 px-2 py-2 md:flex-wrap md:gap-4 md:px-5 md:py-3">
<MobileNavigation />
{leading && <div className="flex shrink-0 items-center">{leading}</div>}
{leading && <div className="hidden shrink-0 items-center md:flex">{leading}</div>}

Check warning on line 25 in application/v2_ui/src/components/layout/PageHeader.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
<div className="min-w-0 flex-1">
<h1 className="truncate text-[15px] font-semibold text-text-1">{title}</h1>
{description && (
<p className="mt-0.5 truncate text-xs text-text-3">{description}</p>
)}
</div>
{actions && <div className="page-header-actions ml-auto flex max-w-full flex-wrap items-center gap-2">{actions}</div>}
{status && <div role="status" className="page-header-status flex shrink-0 items-center">{status}</div>}

Check warning on line 32 in application/v2_ui/src/components/layout/PageHeader.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
{actions && <HeaderActions actions={actions} />}
</header>
);
}
11 changes: 7 additions & 4 deletions application/v2_ui/src/components/tour/GuidedTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function GuidedTour({
[],
);

useEffect(() => {
useLayoutEffect(() => {
const previous = document.activeElement;
return () => {
if (previous instanceof HTMLElement && previous.isConnected) {
Expand Down Expand Up @@ -149,9 +149,12 @@ export function GuidedTour({
};
}, [reposition]);

useEffect(() => {
cardRef.current?.focus();
}, [index]);
const cardVisible = !step || cardPosition !== null;
useLayoutEffect(() => {
if (cardVisible) {
cardRef.current?.focus();
}
}, [index, cardVisible]);

const last = index >= steps.length - 1;
const next = useCallback(() => {
Expand Down
34 changes: 23 additions & 11 deletions application/v2_ui/src/components/tour/TourLauncher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { CircleHelp } from 'lucide-react';
import { useUserSettingsStore } from '../../stores/userSettingsStore';
import { consumeTourRequest, findTour, isTourEnabled } from '../../lib/tours';
import { GuidedTour } from './GuidedTour';
import type { HeaderAction } from '../layout/HeaderActions';

export function TourLauncher({ tourId, className }: { tourId: string; className?: string }) {
export function useTourLauncher(tourId: string, available = true) {
const tour = findTour(tourId);
const [open, setOpen] = useState(false);
const settingsLoading = useUserSettingsStore((state) => state.loading);
Expand All @@ -23,6 +24,7 @@ export function TourLauncher({ tourId, className }: { tourId: string; className?
);

useEffect(() => {
if (!available) return;
// Gives the page a moment to draw the controls the tour points at. The request is
// consumed inside the timer so a mount that is torn down early leaves it in place.
const timer = window.setTimeout(() => {
Expand All @@ -31,29 +33,39 @@ export function TourLauncher({ tourId, className }: { tourId: string; className?
}
}, 400);
return () => window.clearTimeout(timer);
}, [tourId]);
}, [tourId, available]);

const label = tour ? `Take the ${tour.title.toLowerCase()}` : '';
const action: HeaderAction | null = tour && available && enabled && !settingsLoading ? {
id: `tour-${tourId}`, label, icon: <CircleHelp size={17} />,
onClick: () => setOpen(true), iconOnly: true,
} : null;
return {
action,
dialog: tour && open ? <GuidedTour tour={tour} onClose={() => setOpen(false)} /> : null,
};
}

if (!tour) {
return null;
}
export function TourLauncher({ tourId, className }: { tourId: string; className?: string }) {
const { action, dialog } = useTourLauncher(tourId);

return (
<>
{enabled && !settingsLoading && (
{action && (
<button
type="button"
onClick={() => setOpen(true)}
title={`Take the ${tour.title.toLowerCase()}`}
aria-label={`Take the ${tour.title.toLowerCase()}`}
onClick={action.onClick}
title={action.label}
aria-label={action.label}
className={clsx(
'rounded-lg p-2 text-text-3 transition-colors hover:bg-surface-2 hover:text-text-1',
'shrink-0 rounded-lg p-2 text-text-3 transition-colors hover:bg-surface-2 hover:text-text-1 max-md:min-h-11 max-md:min-w-11',
className,
)}
>
<CircleHelp size={17} />
</button>
)}
{open && <GuidedTour tour={tour} onClose={() => setOpen(false)} />}
{dialog}
</>
);
}
96 changes: 96 additions & 0 deletions application/v2_ui/src/components/ui/ActionPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// ActionPopover.tsx
// A bounded overlay for header actions; its host stays outside clipped page panes.

import { useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from 'react';
import { createPortal } from 'react-dom';

export function ActionPopover({
id, label, anchorRef, onClose, children, inline = false,
}: {
id: string;
label: string;
anchorRef: RefObject<HTMLButtonElement>;
onClose: (returnFocus: boolean) => void;
children: ReactNode;
inline?: boolean;
}) {
const panelRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: 0, left: 8, maxHeight: 320, width: 272 });

useLayoutEffect(() => {
if (inline) return;
const measure = () => {
const anchor = anchorRef.current;
if (!anchor) return;
const rect = anchor.getBoundingClientRect();
const width = Math.min(272, window.innerWidth - 16);
const top = Math.min(rect.bottom + 8, window.innerHeight - 52);
setPosition({
top, width,
left: Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8)),
maxHeight: Math.max(44, window.innerHeight - top - 8),
});
};
measure();
const first = Array.from(panelRef.current?.querySelectorAll<HTMLElement>(
'button:not(:disabled), a[href]',

Check warning on line 36 in application/v2_ui/src/components/ui/ActionPopover.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
) ?? []).find((control) => control.getClientRects().length > 0);
(first ?? panelRef.current)?.focus();
const outside = (event: PointerEvent) => {
if (event.target instanceof Node &&
!panelRef.current?.contains(event.target) && !anchorRef.current?.contains(event.target)) {
onClose(false);
}
};
const focusOutside = (event: FocusEvent) => {
if (event.target instanceof Node &&
!panelRef.current?.contains(event.target) && !anchorRef.current?.contains(event.target)) {
onClose(false);
}
};
document.addEventListener('pointerdown', outside);
document.addEventListener('focusin', focusOutside);
window.addEventListener('resize', measure);
window.addEventListener('scroll', measure, true);
return () => {
document.removeEventListener('pointerdown', outside);
document.removeEventListener('focusin', focusOutside);
window.removeEventListener('resize', measure);
window.removeEventListener('scroll', measure, true);
};
}, [anchorRef, onClose, inline]);

if (inline) return <>{children}</>;
return createPortal(
<div ref={panelRef} id={id} role="group" aria-label={label} tabIndex={-1}

Check warning on line 65 in application/v2_ui/src/components/ui/ActionPopover.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
style={position}
className="header-action-popover glass-modal fixed z-[100] overflow-y-auto rounded-xl p-1.5"
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
onClose(true);
}
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Tab'].includes(event.key)) {
const controls = Array.from(event.currentTarget.querySelectorAll<HTMLElement>(
'button:not(:disabled), a[href]',

Check warning on line 76 in application/v2_ui/src/components/ui/ActionPopover.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
)).filter((control) => control.getClientRects().length > 0);
const current = controls.indexOf(document.activeElement as HTMLElement);
if (event.key === 'Tab') {
if ((event.shiftKey && current === 0) || (!event.shiftKey && current === controls.length - 1)) {
event.preventDefault();
onClose(true);
}
return;
}
const next = event.key === 'Home' ? 0 : event.key === 'End' ? controls.length - 1
: (current + (event.key === 'ArrowDown' ? 1 : -1) + controls.length) % controls.length;
event.preventDefault();
controls[next]?.focus();
}
}}>
{children}
</div>,
document.body,
);
}
17 changes: 5 additions & 12 deletions application/v2_ui/src/dev/AlertLabPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,11 @@
title="Alert lab"
description="Dev only. Sample workflow alerts in the real shell; nothing reaches the server."
leading={<FlaskConical size={18} className="text-accent" aria-hidden="true" />}
actions={(
<>
<GlassButton type="button" size="sm" variant="subtle" onClick={() => lastFeed.current?.()} disabled={!lastFeed.current}>
<RotateCcw size={14} aria-hidden="true" />
Replay
</GlassButton>
<GlassButton type="button" size="sm" variant="subtle" onClick={clear}>
<Eraser size={14} aria-hidden="true" />
Clear
</GlassButton>
</>
)}
actions={[
{ id: 'replay', label: 'Replay', icon: <RotateCcw size={14} />, variant: 'subtle',
onClick: () => lastFeed.current?.(), disabled: !lastFeed.current },

Check warning on line 249 in application/v2_ui/src/dev/AlertLabPage.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
{ id: 'clear', label: 'Clear', icon: <Eraser size={14} />, variant: 'subtle', onClick: clear },
]}
/>
<div className="min-h-0 flex-1 overflow-y-auto p-4 md:p-5">
<div className="mx-auto grid max-w-5xl gap-4 lg:grid-cols-2">
Expand Down
13 changes: 13 additions & 0 deletions application/v2_ui/src/lib/useMobileViewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// useMobileViewport.ts
import { useEffect, useState } from 'react';

export function useMobileViewport() {
const [mobile, setMobile] = useState(() => window.matchMedia('(max-width: 767px)').matches);
useEffect(() => {
const query = window.matchMedia('(max-width: 767px)');
const update = () => setMobile(query.matches);
query.addEventListener('change', update);
return () => query.removeEventListener('change', update);
}, []);
return mobile;
}
Loading
Loading