mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
feat(policies): backend-driven policy enforcement (frontend) (#6598)
## Summary Adds the **Policies** feature (proprietary, behind the `POLICIES_ENABLED` flag): backend-driven enforcement that runs a fixed tool pipeline on documents, docked in the right tool sidebar alongside Tools. ## Highlights - **Policy catalog** — 5 categories; **Security** is wired (redact PII + sanitize), the others are marked "Coming soon". - **Backend as source of truth** — policies persist via the Policies engine (`/api/v1/policies`), one policy per category, with a local cache + offline fallback. - **Auto-run** — enabled policies run on every uploaded file: dispatch → poll → import outputs into the workspace. - **Security redact config** — PII preset dropdown + custom word/regex entry + advanced options; tool params map to the backend endpoint fields. - **Activity feed** with retry on failures; **file badges** showing which policies ran on a file (sidebar + files page), tinted to the policy colour. - Reuses the **Watched Folders** engine for each policy's backing folder; policy-owned folders are filtered out of the Watched Folders UI. ## Notes - Gated by `POLICIES_ENABLED` (true in proprietary, false in core) — unreachable in the open-source build. - Frontend-only diff; depends on the backend Policies engine and the merged Watched Folders feature.
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActionIcon, Button, Checkbox, Menu, Tooltip } from "@mantine/core";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
@@ -17,6 +18,7 @@ import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import { FileId } from "@app/types/file";
|
||||
import { FolderId, FolderRecord, ROOT_FOLDER_ID } from "@app/types/folder";
|
||||
import { useFolders } from "@app/contexts/FolderContext";
|
||||
import { usePolicyFileBadges } from "@app/hooks/usePolicyFileBadges";
|
||||
import { StirlingFileStub } from "@app/types/fileContext";
|
||||
import { formatFileSize, getFileDate } from "@app/utils/fileUtils";
|
||||
import {
|
||||
@@ -572,6 +574,31 @@ function FolderCard({
|
||||
);
|
||||
}
|
||||
|
||||
/** Shield badges for the policies that have run on a file. */
|
||||
function PolicyBadges({ fileId }: { fileId: string }) {
|
||||
const badges = usePolicyFileBadges().get(fileId) ?? [];
|
||||
if (badges.length === 0) return null;
|
||||
return (
|
||||
<span className="files-page-policy-badges" data-no-select>
|
||||
{badges.slice(0, 3).map((policy) => (
|
||||
<Tooltip
|
||||
key={policy.id}
|
||||
label={`${policy.name} policy ran on this file`}
|
||||
withArrow
|
||||
position="top"
|
||||
>
|
||||
<span
|
||||
className="files-page-policy-badge"
|
||||
style={{ color: policy.accentColor }}
|
||||
>
|
||||
<ShieldOutlinedIcon sx={{ fontSize: "0.7rem" }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface FileCardProps {
|
||||
file: StirlingFileStub;
|
||||
isSelected: boolean;
|
||||
@@ -731,6 +758,7 @@ function FileCard({
|
||||
<span>{fileSize}</span>
|
||||
<span>·</span>
|
||||
<span>{fileDate}</span>
|
||||
<PolicyBadges fileId={file.id as string} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="files-page-card-actions">
|
||||
@@ -1315,6 +1343,7 @@ function FileRow({
|
||||
)}
|
||||
</span>
|
||||
<FileOriginBadge origin={getFileOrigin(file)} compact />
|
||||
<PolicyBadges fileId={file.id as string} />
|
||||
{isInWorkspace && (
|
||||
<span className="files-page-row-open-pill">
|
||||
<span className="files-page-card-open-dot" />
|
||||
|
||||
@@ -484,6 +484,24 @@
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* Policy activity badges (a shield per policy that has run on the file). */
|
||||
.files-page-policy-badges {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.files-page-policy-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 4px;
|
||||
/* `color` set inline to the policy accent; tint follows it. */
|
||||
background: color-mix(in srgb, currentColor 16%, transparent);
|
||||
}
|
||||
|
||||
/* Parent-folder breadcrumb shown on cards/rows during recursive search so
|
||||
the user can tell which folder each hit lives in without navigating. */
|
||||
.files-page-card-path {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Core stubs for the right-rail Policies UI.
|
||||
*
|
||||
* The real implementations live in {@code proprietary/components/policies/PoliciesSidebar.tsx}
|
||||
* and shadow these stubs via the {@code @app/*} alias cascade when the proprietary
|
||||
* build is active. Core builds render nothing, so the right rail shows only the
|
||||
* tool list unchanged.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/** Whether the right rail should host the Policies section. False in core. */
|
||||
export function usePoliciesEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a policy is open (its detail should take over the rail). Always false
|
||||
* in core; proprietary bridges to the policy-selection store.
|
||||
*/
|
||||
export function usePolicyDetailActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Collapsible policy list rendered above the Tools section. Null in core. */
|
||||
export function PoliciesSection(_props: { leadingControl?: ReactNode } = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Open-policy detail/wizard/settings that replaces the tool area. Null in core. */
|
||||
export function PolicyDetailTakeover() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Collapsed-rail policy icons. Null in core; proprietary renders the rail. */
|
||||
export function PoliciesCollapsedButton(_props: { onExpand: () => void }) {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Core stub for the policy auto-run controller.
|
||||
*
|
||||
* The real implementation lives in
|
||||
* {@code proprietary/components/policies/PolicyAutoRunController.tsx} and shadows
|
||||
* this stub via the {@code @app/*} alias cascade in the proprietary build. Core
|
||||
* builds have no Policies feature, so this renders nothing and runs no policies.
|
||||
*/
|
||||
export function PolicyAutoRunController() {
|
||||
return null;
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import type { FileId } from "@app/types/file";
|
||||
import { FileItem } from "@app/components/shared/FileSidebarFileItem";
|
||||
import { useFolderMembership } from "@app/hooks/useFolderMembership";
|
||||
import { useAllWatchedFolders } from "@app/hooks/useAllWatchedFolders";
|
||||
import { usePolicyFileBadges } from "@app/hooks/usePolicyFileBadges";
|
||||
import {
|
||||
setWatchedFolderDraggedFileIds,
|
||||
clearWatchedFolderDraggedFileIds,
|
||||
@@ -131,6 +132,7 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
|
||||
// the workbench). The same map drives the per-file membership dots.
|
||||
const folderMembership = useFolderMembership();
|
||||
const allFolders = useAllWatchedFolders();
|
||||
const policyFileBadges = usePolicyFileBadges();
|
||||
const folderById = useMemo(
|
||||
() => new Map(allFolders.map((f) => [f.id, f])),
|
||||
[allFolders],
|
||||
@@ -892,6 +894,9 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
|
||||
onDragStart={handleWatchedFolderDragStart}
|
||||
folders={memberFolders}
|
||||
onFolderClick={openWatchedFolder}
|
||||
policies={
|
||||
policyFileBadges.get(stub.id as string) ?? []
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -134,6 +134,25 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ---- Policy activity badges (a shield per policy that has run on the file) ---- */
|
||||
.file-sidebar-policy-badges {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-sidebar-policy-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 4px;
|
||||
/* `color` is set inline to the policy's accent; the tint follows it. */
|
||||
color: var(--text-secondary);
|
||||
background: color-mix(in srgb, currentColor 16%, transparent);
|
||||
}
|
||||
|
||||
/* ---- Folder membership tags ---- */
|
||||
.file-sidebar-folder-tags {
|
||||
display: flex;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Tooltip } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
import type { FileId } from "@app/types/file";
|
||||
import { FileDocIcon } from "@app/components/shared/FileDocIcon";
|
||||
import { getFileDocVariant } from "@app/components/shared/filePreview/getFileTypeIcon";
|
||||
@@ -125,6 +126,14 @@ export interface FileItemFolderRef {
|
||||
accentColor: string;
|
||||
}
|
||||
|
||||
/** A policy that has run on this file, used for the activity badges. */
|
||||
export interface FileItemPolicyRef {
|
||||
id: string;
|
||||
name: string;
|
||||
/** CSS colour for the badge (matches the policy's accent). */
|
||||
accentColor: string;
|
||||
}
|
||||
|
||||
export interface FileItemProps {
|
||||
fileId: FileId;
|
||||
name: string;
|
||||
@@ -143,9 +152,12 @@ export interface FileItemProps {
|
||||
folders?: FileItemFolderRef[];
|
||||
/** Clicking a membership dot opens that folder. */
|
||||
onFolderClick?: (folderId: string) => void;
|
||||
/** Policies that have run on this file — rendered as small shield badges. */
|
||||
policies?: FileItemPolicyRef[];
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_FOLDER_TAGS = 2;
|
||||
const MAX_VISIBLE_POLICY_BADGES = 3;
|
||||
|
||||
export function FileItem({
|
||||
fileId,
|
||||
@@ -162,6 +174,7 @@ export function FileItem({
|
||||
onDragStart,
|
||||
folders = [],
|
||||
onFolderClick,
|
||||
policies = [],
|
||||
}: FileItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const ext = getFileExtension(name);
|
||||
@@ -237,6 +250,25 @@ export function FileItem({
|
||||
{dateLabel && typeLabel ? " · " : ""}
|
||||
{typeLabel}
|
||||
</span>
|
||||
{policies.length > 0 && (
|
||||
<span className="file-sidebar-policy-badges" data-no-select>
|
||||
{policies.slice(0, MAX_VISIBLE_POLICY_BADGES).map((policy) => (
|
||||
<Tooltip
|
||||
key={policy.id}
|
||||
label={`${policy.name} policy ran on this file`}
|
||||
withArrow
|
||||
position="top"
|
||||
>
|
||||
<span
|
||||
className="file-sidebar-policy-badge"
|
||||
style={{ color: policy.accentColor }}
|
||||
>
|
||||
<ShieldOutlinedIcon sx={{ fontSize: "0.7rem" }} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{folders.length > 0 && (
|
||||
<span className="file-sidebar-folder-tags" data-no-select>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, ReactNode } from "react";
|
||||
import { createContext, useContext, useEffect, ReactNode } from "react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { useRainbowTheme } from "@app/hooks/useRainbowTheme";
|
||||
import { mantineTheme } from "@app/theme/mantineTheme";
|
||||
@@ -7,6 +7,10 @@ import { ToastProvider } from "@app/components/toast";
|
||||
import ToastRenderer from "@app/components/toast/ToastRenderer";
|
||||
import { ToastPortalBinder } from "@app/components/toast";
|
||||
import type { ThemeMode } from "@app/constants/theme";
|
||||
// SUI shared design-system tokens (used by @shared/components). Additive — its
|
||||
// var names don't clash with the editor's own theme.css. The effect below
|
||||
// bridges Mantine's color scheme to the `data-theme` attribute SUI keys on.
|
||||
import "@shared/tokens/tokens.css";
|
||||
|
||||
interface RainbowThemeContextType {
|
||||
themeMode: ThemeMode;
|
||||
@@ -40,6 +44,16 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) {
|
||||
const mantineColorScheme =
|
||||
rainbowTheme.themeMode === "rainbow" ? "dark" : rainbowTheme.themeMode;
|
||||
|
||||
// Bridge the resolved scheme to the `data-theme` attribute SUI's tokens.css
|
||||
// keys its dark palette on (the editor otherwise only sets Mantine's
|
||||
// data-mantine-color-scheme), so @shared/components theme correctly.
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
mantineColorScheme === "dark" ? "dark" : "light",
|
||||
);
|
||||
}, [mantineColorScheme]);
|
||||
|
||||
return (
|
||||
<RainbowThemeContext.Provider value={rainbowTheme}>
|
||||
<MantineProvider
|
||||
|
||||
@@ -13,6 +13,14 @@ import {
|
||||
AgentsSection,
|
||||
useAgentsEnabled,
|
||||
} from "@app/components/agents/AgentsPanel";
|
||||
import {
|
||||
PoliciesCollapsedButton,
|
||||
PoliciesSection,
|
||||
PolicyDetailTakeover,
|
||||
usePoliciesEnabled,
|
||||
usePolicyDetailActive,
|
||||
} from "@app/components/policies/PoliciesSidebar";
|
||||
import { PolicyAutoRunController } from "@app/components/policies/PolicyAutoRunController";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import { ChatPanel } from "@app/components/chat/ChatPanel";
|
||||
import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems";
|
||||
@@ -70,6 +78,8 @@ export default function RightSidebar() {
|
||||
} = useToolWorkflow();
|
||||
|
||||
const agentsEnabled = useAgentsEnabled();
|
||||
const policiesEnabled = usePoliciesEnabled();
|
||||
const rawPolicyDetailActive = usePolicyDetailActive();
|
||||
const fullscreenExpanded = useIsFullscreenExpanded();
|
||||
const fullscreenGeometry = useToolPanelGeometry({
|
||||
enabled: fullscreenExpanded,
|
||||
@@ -153,15 +163,35 @@ export default function RightSidebar() {
|
||||
});
|
||||
};
|
||||
|
||||
// Opening a policy (e.g. from the collapsed rail) lands the rail in the clean
|
||||
// default tool-picker view — the only view the policy takeover renders in — so
|
||||
// it never collides with an open tool or the all-tools/search view.
|
||||
const handleOpenPolicy = () => {
|
||||
withViewTransition(() => {
|
||||
if (readerMode) setReaderMode(false);
|
||||
setLeftPanelView("toolPicker");
|
||||
if (!sidebarsVisible) setSidebarsVisible(true);
|
||||
setAllToolsView(false);
|
||||
setSearchQuery("");
|
||||
});
|
||||
};
|
||||
|
||||
// The header shows [back] [search] when we have somewhere to go back to —
|
||||
// i.e. the user is in a specific tool, or already in the all-tools/search view.
|
||||
const inToolView = leftPanelView !== "toolPicker";
|
||||
// Show X (close) button only when there's somewhere to go back to.
|
||||
const showCloseButton = inToolView || allToolsView;
|
||||
// Show search input whenever there's a close button, or when agents are off and
|
||||
// we're in the default tool-picker view (search filters the full list inline).
|
||||
// Policies sit above the tool list in the default tool-picker view.
|
||||
const showPolicies =
|
||||
policiesEnabled && !allToolsView && leftPanelView === "toolPicker";
|
||||
// When Policies are shown, the search moves OUT of the header to sit between
|
||||
// the Policies and Tools sections (separating them); otherwise it stays in the
|
||||
// header. Show the header search when there's a close button, or when agents
|
||||
// are off in the default tool-picker view (inline filtering).
|
||||
const showInlineSearch = showPolicies && !showCloseButton;
|
||||
const showHeaderSearch =
|
||||
showCloseButton || (!agentsEnabled && leftPanelView === "toolPicker");
|
||||
!showInlineSearch &&
|
||||
(showCloseButton || (!agentsEnabled && leftPanelView === "toolPicker"));
|
||||
|
||||
const handleHeaderBack = () => {
|
||||
if (inToolView) {
|
||||
@@ -201,11 +231,21 @@ export default function RightSidebar() {
|
||||
const showAgents =
|
||||
agentsEnabled && !allToolsView && leftPanelView === "toolPicker";
|
||||
|
||||
// The detail takeover replaces the tool list ONLY in the same default view —
|
||||
// never over an open tool or the all-tools view (which must keep priority).
|
||||
// A lingering selection is harmless: it stays hidden behind a tool and the
|
||||
// list/takeover reappears on return to the picker (as in the prototype).
|
||||
const policyDetailActive = rawPolicyDetailActive && showPolicies;
|
||||
|
||||
// The rail widens when a policy detail takes it over — the tool list is fine
|
||||
// at 18.5rem, but the policy detail/wizard/settings need more breathing room.
|
||||
const expandedWidth = policyDetailActive ? "25rem" : "18.5rem";
|
||||
|
||||
const computedWidth = () => {
|
||||
if (isMobile) return "100%";
|
||||
if (isChatOpen) return `${chatWidthPx}px`;
|
||||
if (!isPanelVisible) return "3.5rem";
|
||||
return "18.5rem";
|
||||
return expandedWidth;
|
||||
};
|
||||
|
||||
// Collapsed rail: show favourites + recommended tools as icons.
|
||||
@@ -248,6 +288,8 @@ export default function RightSidebar() {
|
||||
...(isChatDragging ? { transition: "none" } : {}),
|
||||
}}
|
||||
>
|
||||
{/* Headless: enforces enabled policies on every uploaded file. */}
|
||||
{policiesEnabled && <PolicyAutoRunController />}
|
||||
{!fullscreenExpanded && isChatOpen && (
|
||||
<div
|
||||
style={{
|
||||
@@ -279,7 +321,7 @@ export default function RightSidebar() {
|
||||
color="gray.4"
|
||||
radius="xl"
|
||||
size="md"
|
||||
className="tool-panel__expand-btn"
|
||||
className="tool-panel__expand-btn tool-panel__toggle-vt"
|
||||
onClick={handleExpand}
|
||||
aria-label={t("toolPanel.expand", "Expand panel")}
|
||||
>
|
||||
@@ -288,6 +330,7 @@ export default function RightSidebar() {
|
||||
<AgentsCollapsedButton onExpand={handleExpand} />
|
||||
</div>
|
||||
<div className="tool-panel__collapsed-divider" />
|
||||
<PoliciesCollapsedButton onExpand={handleOpenPolicy} />
|
||||
<div className="tool-panel__collapsed-tools">
|
||||
{collapsedRailItems.map(({ id, tool }) => (
|
||||
<AppTooltip
|
||||
@@ -326,84 +369,122 @@ export default function RightSidebar() {
|
||||
opacity: 1,
|
||||
transition: "opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
|
||||
height: "100%",
|
||||
width: isMobile ? "100%" : "18.5rem",
|
||||
width: isMobile ? "100%" : expandedWidth,
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div className="tool-panel__compact-header">
|
||||
{activeTool ? (
|
||||
<div
|
||||
className="tool-panel__active-tool-pill"
|
||||
aria-label={activeTool.name}
|
||||
>
|
||||
<span className="tool-panel__active-tool-pill-icon">
|
||||
<ToolIcon
|
||||
icon={activeTool.icon}
|
||||
marginRight="0"
|
||||
color="var(--mantine-color-blue-filled)"
|
||||
/>
|
||||
</span>
|
||||
<span className="tool-panel__active-tool-pill-label">
|
||||
{activeTool.name}
|
||||
</span>
|
||||
</div>
|
||||
) : showHeaderSearch ? (
|
||||
<div className="tool-panel__compact-header-search">
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={handleHeaderSearchChange}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
autoFocus={allToolsView && !inToolView}
|
||||
{policyDetailActive ? (
|
||||
<div className="pol-takeover">
|
||||
<PolicyDetailTakeover />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!showPolicies && (
|
||||
<div className="tool-panel__compact-header">
|
||||
{activeTool ? (
|
||||
<div
|
||||
className="tool-panel__active-tool-pill"
|
||||
aria-label={activeTool.name}
|
||||
>
|
||||
<span className="tool-panel__active-tool-pill-icon">
|
||||
<ToolIcon
|
||||
icon={activeTool.icon}
|
||||
marginRight="0"
|
||||
color="var(--mantine-color-blue-filled)"
|
||||
/>
|
||||
</span>
|
||||
<span className="tool-panel__active-tool-pill-label">
|
||||
{activeTool.name}
|
||||
</span>
|
||||
</div>
|
||||
) : showHeaderSearch ? (
|
||||
<div className="tool-panel__compact-header-search">
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={handleHeaderSearchChange}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
autoFocus={allToolsView && !inToolView}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
showAgents && (
|
||||
<span className="tool-panel__section-label">
|
||||
{t("agents.section_title", "Agents")}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{showCloseButton ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={handleHeaderBack}
|
||||
aria-label={
|
||||
inToolView
|
||||
? t("toolPanel.backToAllTools", "Back to all tools")
|
||||
: t("toolPanel.goBack", "Go back")
|
||||
}
|
||||
className="tool-panel__expand-btn"
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={handleCollapse}
|
||||
aria-label={t("toolPanel.collapse", "Collapse panel")}
|
||||
className="tool-panel__expand-btn tool-panel__toggle-vt"
|
||||
>
|
||||
<ChevronRightIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && <AgentsSection />}
|
||||
|
||||
{showPolicies && (
|
||||
<PoliciesSection
|
||||
leadingControl={
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={handleCollapse}
|
||||
aria-label={t("toolPanel.collapse", "Collapse panel")}
|
||||
className="tool-panel__expand-btn tool-panel__toggle-vt"
|
||||
>
|
||||
<ChevronRightIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
showAgents && (
|
||||
<span className="tool-panel__section-label">
|
||||
{t("agents.section_title", "Agents")}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{showCloseButton ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={handleHeaderBack}
|
||||
aria-label={
|
||||
inToolView
|
||||
? t("toolPanel.backToAllTools", "Back to all tools")
|
||||
: t("toolPanel.goBack", "Go back")
|
||||
}
|
||||
className="tool-panel__expand-btn"
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={handleCollapse}
|
||||
aria-label={t("toolPanel.collapse", "Collapse panel")}
|
||||
className="tool-panel__expand-btn"
|
||||
>
|
||||
<ChevronRightIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && <AgentsSection />}
|
||||
{showInlineSearch && (
|
||||
<div className="tool-panel__between-search">
|
||||
<ToolSearch
|
||||
value={searchQuery}
|
||||
onChange={handleHeaderSearchChange}
|
||||
toolRegistry={toolRegistry}
|
||||
mode="filter"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ToolPanel
|
||||
allToolsView={allToolsView}
|
||||
onShowAllTools={handleShowAllTools}
|
||||
onToolSelect={handleToolSelectWithTransition}
|
||||
compact={agentsEnabled && !allToolsView}
|
||||
/>
|
||||
<ToolPanel
|
||||
allToolsView={allToolsView}
|
||||
onShowAllTools={handleShowAllTools}
|
||||
onToolSelect={handleToolSelectWithTransition}
|
||||
compact={agentsEnabled && !allToolsView}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -201,6 +201,13 @@
|
||||
border-color: var(--border-subtle) !important;
|
||||
}
|
||||
|
||||
/* The collapse/expand toggle keeps a stable identity across the collapsed strip
|
||||
and the expanded header, so a view transition translates it between the two
|
||||
(same size) instead of cross-fading/scaling it ("big then small"). */
|
||||
.tool-panel__toggle-vt {
|
||||
view-transition-name: sidebar-toggle;
|
||||
}
|
||||
|
||||
.tool-panel__expand-btn svg {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
@@ -271,6 +278,22 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Search that separates the Policies section from the Tools list below it. */
|
||||
.tool-panel__between-search {
|
||||
padding: 0.5rem 0.75rem 0.25rem;
|
||||
border-top: 1px solid var(--border-subtle, var(--color-border));
|
||||
}
|
||||
|
||||
.tool-panel__between-search .search-input-container {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Slightly recessed search field so it reads as distinct from the panel. */
|
||||
.tool-panel__between-search input {
|
||||
background-color: var(--bg-muted);
|
||||
}
|
||||
|
||||
.tool-panel__active-tool-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Watched Folders (a.k.a. Watched Folders). Disabled for now — gates both the
|
||||
* custom workbench registration (seeding, URL sync, view) and the sidebar
|
||||
* entry point, so the feature is unreachable from the UI. Flip to `true` to
|
||||
* restore access.
|
||||
* Watched Folders. Disabled for now — gates both the custom workbench
|
||||
* registration (seeding, URL sync, view) and the sidebar entry point, so the
|
||||
* feature is unreachable from the UI. Flip to `true` to restore access.
|
||||
*/
|
||||
// Annotated as `boolean` (not the literal `false`) so call sites aren't treated
|
||||
// as constant/unreachable conditions by the type checker and linter.
|
||||
export const WATCHED_FOLDERS_ENABLED: boolean = false;
|
||||
|
||||
/**
|
||||
* Policies — a proprietary, automation-backed feature (like Watched Folders but
|
||||
* backend-driven, with non-folder triggers). The implementation lives under
|
||||
* `proprietary/`; this core value stays `false` so the shared sidebar entry
|
||||
* never appears in the open-source build.
|
||||
*/
|
||||
export const POLICIES_ENABLED: boolean = false;
|
||||
|
||||
@@ -17,7 +17,9 @@ export function useAllWatchedFolders(): WatchedFolder[] {
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
setFolders(await watchedFolderStorage.getAllFolders());
|
||||
// Policy-owned folders are managed by Policies, not shown here.
|
||||
const all = await watchedFolderStorage.getAllFolders();
|
||||
setFolders(all.filter((f) => !f.policyCategoryId));
|
||||
} catch (err) {
|
||||
console.error("Failed to load smart folders:", err);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { FileItemPolicyRef } from "@app/components/shared/FileSidebarFileItem";
|
||||
|
||||
/**
|
||||
* Policies that have run on each file, keyed by fileId — drives the shield
|
||||
* badges in the file sidebar. Empty in core; the proprietary build shadows this
|
||||
* with an implementation backed by the policy run store.
|
||||
*/
|
||||
export function usePolicyFileBadges(): Map<string, FileItemPolicyRef[]> {
|
||||
return new Map();
|
||||
}
|
||||
@@ -248,6 +248,14 @@ export async function mockAppApis(
|
||||
await page.route("**/api/v1/info/wau", (route: Route) =>
|
||||
route.fulfill({ json: { count: 0 } }),
|
||||
);
|
||||
|
||||
// Policies (proprietary): the reconcile fires GET /api/v1/policies on app
|
||||
// load. Return an empty list so the stubbed (backend-free) env stays clean —
|
||||
// no failed request polluting the console, and the auto-run controller has
|
||||
// nothing to dispatch.
|
||||
await page.route("**/api/v1/policies", (route: Route) =>
|
||||
route.fulfill({ json: [] }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,9 @@ export interface WatchedFolder {
|
||||
hasOutputDirectory?: boolean; // true when a local FS output folder is configured
|
||||
/** Where input files come from. Default: 'idb' (dropped/sidebar files stay in browser). */
|
||||
inputSource?: "idb" | "local-folder";
|
||||
/** Set when this folder is the backing store for a Policy (its category id).
|
||||
* Policy-owned folders are filtered out of the Watched Folders UI. */
|
||||
policyCategoryId?: string;
|
||||
}
|
||||
|
||||
export interface FolderFileMetadata {
|
||||
|
||||
Reference in New Issue
Block a user