Align connectors, editors, and layout with desktop context

This commit is contained in:
CREDO23 2026-04-24 19:19:04 +02:00
parent 8034f372e7
commit ed0bcafe49
20 changed files with 243 additions and 201 deletions

View file

@ -123,9 +123,9 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
handleSkipIndexing, handleSkipIndexing,
handleStartEdit, handleStartEdit,
handleSaveConnector, handleSaveConnector,
handleDisconnectConnector, handleDisconnectConnector,
handleDisconnectFromList, handleDisconnectFromList,
handleBackFromEdit, handleBackFromEdit,
handleBackFromConnect, handleBackFromConnect,
handleBackFromYouTube, handleBackFromYouTube,
handleViewAccountsList, handleViewAccountsList,
@ -226,27 +226,31 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
{isYouTubeView && searchSpaceId ? ( {isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} /> <YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? ( ) : viewingMCPList ? (
<ConnectorAccountsListView <ConnectorAccountsListView
connectorType="MCP_CONNECTOR" connectorType="MCP_CONNECTOR"
connectorTitle="MCP Connectors" connectorTitle="MCP Connectors"
connectors={(allConnectors || []) as SearchSourceConnector[]} connectors={(allConnectors || []) as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds} indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromMCPList} onBack={handleBackFromMCPList}
onManage={handleStartEdit} onManage={handleStartEdit}
onDisconnect={(connector) => handleDisconnectFromList(connector, () => refreshConnectors())} onDisconnect={(connector) =>
onAddAccount={handleAddNewMCPFromList} handleDisconnectFromList(connector, () => refreshConnectors())
addButtonText="Add New MCP Server" }
/> onAddAccount={handleAddNewMCPFromList}
addButtonText="Add New MCP Server"
/>
) : viewingAccountsType ? ( ) : viewingAccountsType ? (
<ConnectorAccountsListView <ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType} connectorType={viewingAccountsType.connectorType}
connectorTitle={viewingAccountsType.connectorTitle} connectorTitle={viewingAccountsType.connectorTitle}
connectors={(connectors || []) as SearchSourceConnector[]} connectors={(connectors || []) as SearchSourceConnector[]}
indexingConnectorIds={indexingConnectorIds} indexingConnectorIds={indexingConnectorIds}
onBack={handleBackFromAccountsList} onBack={handleBackFromAccountsList}
onManage={handleStartEdit} onManage={handleStartEdit}
onDisconnect={(connector) => handleDisconnectFromList(connector, () => refreshConnectors())} onDisconnect={(connector) =>
onAddAccount={() => { handleDisconnectFromList(connector, () => refreshConnectors())
}
onAddAccount={() => {
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS // Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
const oauthConnector = const oauthConnector =
OAUTH_CONNECTORS.find( OAUTH_CONNECTORS.find(

View file

@ -213,13 +213,13 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
className="w-full h-8 text-[13px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80" className="w-full h-8 text-[13px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80"
> >
{isTesting ? ( {isTesting ? (
<> <>
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
Testing Connection... Testing Connection...
</> </>
) : ( ) : (
"Test Connection" "Test Connection"
)} )}
</Button> </Button>
</div> </div>

View file

@ -218,13 +218,13 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
className="w-full h-8 text-[13px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80" className="w-full h-8 text-[13px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80"
> >
{isTesting ? ( {isTesting ? (
<> <>
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
Testing Connection... Testing Connection...
</> </>
) : ( ) : (
"Test Connection" "Test Connection"
)} )}
</Button> </Button>
</div> </div>

View file

@ -18,9 +18,9 @@ export const TeamsConfig: FC<TeamsConfigProps> = () => {
<div className="text-xs sm:text-sm"> <div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Microsoft Teams Access</p> <p className="font-medium text-xs sm:text-sm">Microsoft Teams Access</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm"> <p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
Your agent can search and read messages from Teams channels you have access to, Your agent can search and read messages from Teams channels you have access to, and send
and send messages on your behalf. Make sure you&#39;re a member of the teams messages on your behalf. Make sure you&#39;re a member of the teams you want to interact
you want to interact with. with.
</p> </p>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@ import { DateRangeSelector } from "../../components/date-range-selector";
import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
import { SummaryConfig } from "../../components/summary-config"; import { SummaryConfig } from "../../components/summary-config";
import { VisionLLMConfig } from "../../components/vision-llm-config"; import { VisionLLMConfig } from "../../components/vision-llm-config";
import { LIVE_CONNECTOR_TYPES, getReauthEndpoint } from "../../constants/connector-constants"; import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { MCPServiceConfig } from "../components/mcp-service-config"; import { MCPServiceConfig } from "../components/mcp-service-config";
import { type ConnectorConfigProps, getConnectorConfigComponent } from "../index"; import { type ConnectorConfigProps, getConnectorConfigComponent } from "../index";
@ -367,8 +367,8 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
{/* Fixed Footer - Action buttons */} {/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-6 sm:py-6 bg-muted border-t border-border"> <div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-6 sm:py-6 bg-muted border-t border-border">
{showDisconnectConfirm ? ( {showDisconnectConfirm ? (
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial">
<span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap"> <span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap">
{isLive {isLive
? "Your agent will lose access to this service." ? "Your agent will lose access to this service."

View file

@ -11,7 +11,10 @@ import { DateRangeSelector } from "../../components/date-range-selector";
import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
import { SummaryConfig } from "../../components/summary-config"; import { SummaryConfig } from "../../components/summary-config";
import { VisionLLMConfig } from "../../components/vision-llm-config"; import { VisionLLMConfig } from "../../components/vision-llm-config";
import { LIVE_CONNECTOR_TYPES, type IndexingConfigState } from "../../constants/connector-constants"; import {
type IndexingConfigState,
LIVE_CONNECTOR_TYPES,
} from "../../constants/connector-constants";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { getConnectorConfigComponent } from "../index"; import { getConnectorConfigComponent } from "../index";

View file

@ -9,7 +9,11 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels"; import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { COMPOSIO_CONNECTORS, LIVE_CONNECTOR_TYPES, OAUTH_CONNECTORS } from "../constants/connector-constants"; import {
COMPOSIO_CONNECTORS,
LIVE_CONNECTOR_TYPES,
OAUTH_CONNECTORS,
} from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import { getConnectorDisplayName } from "./all-connectors-tab"; import { getConnectorDisplayName } from "./all-connectors-tab";

View file

@ -13,7 +13,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { authenticatedFetch } from "@/lib/auth-utils"; import { authenticatedFetch } from "@/lib/auth-utils";
import { formatRelativeDate } from "@/lib/format-date"; import { formatRelativeDate } from "@/lib/format-date";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LIVE_CONNECTOR_TYPES, getReauthEndpoint } from "../constants/connector-constants"; import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status"; import { useConnectorStatus } from "../hooks/use-connector-status";
import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
@ -182,11 +182,14 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{typeConnectors.map((connector) => { {typeConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id); const isIndexing = indexingConnectorIds.has(connector.id);
const connectorReauthEndpoint = getReauthEndpoint(connector); const connectorReauthEndpoint = getReauthEndpoint(connector);
const isAuthExpired = !!connectorReauthEndpoint && connector.config?.auth_expired === true; const isAuthExpired =
const isLive = LIVE_CONNECTOR_TYPES.has(connector.connector_type) || Boolean(connector.config?.server_config); !!connectorReauthEndpoint && connector.config?.auth_expired === true;
const isLive =
LIVE_CONNECTOR_TYPES.has(connector.connector_type) ||
Boolean(connector.config?.server_config);
return ( return (
<div <div
@ -225,73 +228,73 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
</p> </p>
) : null} ) : null}
</div> </div>
{isAuthExpired ? ( {isAuthExpired ? (
<Button <Button
size="sm" size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-amber-600 hover:bg-amber-700 text-white border-0 shadow-xs shrink-0" className="h-8 text-[11px] px-3 rounded-lg font-medium bg-amber-600 hover:bg-amber-700 text-white border-0 shadow-xs shrink-0"
onClick={() => handleReauth(connector)} onClick={() => handleReauth(connector)}
disabled={reauthingId === connector.id} disabled={reauthingId === connector.id}
> >
<RefreshCw <RefreshCw
className={cn("size-3.5", reauthingId === connector.id && "animate-spin")} className={cn("size-3.5", reauthingId === connector.id && "animate-spin")}
/> />
Re-authenticate Re-authenticate
</Button> </Button>
) : isLive && onDisconnect ? ( ) : isLive && onDisconnect ? (
confirmDisconnectId === connector.id ? ( confirmDisconnectId === connector.id ? (
<div className="flex items-center gap-1.5 shrink-0"> <div className="flex items-center gap-1.5 shrink-0">
<Button
variant="destructive"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium shadow-xs"
onClick={async () => {
setDisconnectingId(connector.id);
setConfirmDisconnectId(null);
try {
await onDisconnect(connector);
} finally {
setDisconnectingId(null);
}
}}
disabled={disconnectingId === connector.id}
>
{disconnectingId === connector.id ? (
<RefreshCw className="size-3.5 animate-spin" />
) : (
"Confirm"
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 text-[11px] px-2 rounded-lg"
onClick={() => setConfirmDisconnectId(null)}
disabled={disconnectingId === connector.id}
>
Cancel
</Button>
</div>
) : (
<Button <Button
variant="destructive" variant="secondary"
size="sm" size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium shadow-xs" className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-red-50 hover:text-red-700 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-red-950 dark:hover:text-red-400 shrink-0"
onClick={async () => { onClick={() => setConfirmDisconnectId(connector.id)}
setDisconnectingId(connector.id);
setConfirmDisconnectId(null);
try {
await onDisconnect(connector);
} finally {
setDisconnectingId(null);
}
}}
disabled={disconnectingId === connector.id}
> >
{disconnectingId === connector.id ? ( <Trash2 className="size-3.5" />
<RefreshCw className="size-3.5 animate-spin" /> Disconnect
) : (
"Confirm"
)}
</Button> </Button>
<Button )
variant="ghost"
size="sm"
className="h-8 text-[11px] px-2 rounded-lg"
onClick={() => setConfirmDisconnectId(null)}
disabled={disconnectingId === connector.id}
>
Cancel
</Button>
</div>
) : ( ) : (
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-red-50 hover:text-red-700 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-red-950 dark:hover:text-red-400 shrink-0" className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80 shrink-0"
onClick={() => setConfirmDisconnectId(connector.id)} onClick={() => onManage(connector)}
> >
<Trash2 className="size-3.5" /> Manage
Disconnect
</Button> </Button>
) )}
) : (
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80 shrink-0"
onClick={() => onManage(connector)}
>
Manage
</Button>
)}
</div> </div>
); );
})} })}

View file

@ -20,7 +20,6 @@ import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image"; import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation"; import { InlineCitation, UrlCitation } from "@/components/assistant-ui/inline-citation";
import { useElectronAPI } from "@/hooks/use-platform";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
@ -30,6 +29,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { useElectronAPI } from "@/hooks/use-platform";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function MarkdownCodeBlockSkeleton() { function MarkdownCodeBlockSkeleton() {

View file

@ -226,67 +226,68 @@ export function EditorPanelContent({
} }
}, [editorDoc?.source_markdown]); }, [editorDoc?.source_markdown]);
const handleSave = useCallback(async (options?: { silent?: boolean }) => { const handleSave = useCallback(
setSaving(true); async (options?: { silent?: boolean }) => {
try { setSaving(true);
if (isLocalFileMode) { try {
if (!localFilePath) { if (isLocalFileMode) {
throw new Error("Missing local file path"); if (!localFilePath) {
throw new Error("Missing local file path");
}
if (!electronAPI?.writeAgentLocalFileText) {
throw new Error("Local file editor is available only in desktop mode.");
}
const contentToSave = markdownRef.current;
const writeResult = await electronAPI.writeAgentLocalFileText(
localFilePath,
contentToSave
);
if (!writeResult.ok) {
throw new Error(writeResult.error || "Failed to save local file");
}
setEditorDoc((prev) => (prev ? { ...prev, source_markdown: contentToSave } : prev));
setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current);
return true;
} }
if (!electronAPI?.writeAgentLocalFileText) { if (!searchSpaceId || !documentId) {
throw new Error("Local file editor is available only in desktop mode."); throw new Error("Missing document context");
} }
const contentToSave = markdownRef.current; const token = getBearerToken();
const writeResult = await electronAPI.writeAgentLocalFileText( if (!token) {
localFilePath, toast.error("Please login to save");
contentToSave redirectToLogin();
return;
}
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source_markdown: markdownRef.current }),
}
); );
if (!writeResult.ok) {
throw new Error(writeResult.error || "Failed to save local file"); if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ detail: "Failed to save document" }));
throw new Error(errorData.detail || "Failed to save document");
} }
setEditorDoc((prev) =>
prev ? { ...prev, source_markdown: contentToSave } : prev setEditorDoc((prev) => (prev ? { ...prev, source_markdown: markdownRef.current } : prev));
); setEditedMarkdown(null);
setEditedMarkdown(markdownRef.current === contentToSave ? null : markdownRef.current); toast.success("Document saved! Reindexing in background...");
return true; return true;
} catch (err) {
console.error("Error saving document:", err);
toast.error(err instanceof Error ? err.message : "Failed to save document");
return false;
} finally {
setSaving(false);
} }
if (!searchSpaceId || !documentId) { },
throw new Error("Missing document context"); [documentId, electronAPI, isLocalFileMode, localFilePath, searchSpaceId]
} );
const token = getBearerToken();
if (!token) {
toast.error("Please login to save");
redirectToLogin();
return;
}
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source_markdown: markdownRef.current }),
}
);
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ detail: "Failed to save document" }));
throw new Error(errorData.detail || "Failed to save document");
}
setEditorDoc((prev) => (prev ? { ...prev, source_markdown: markdownRef.current } : prev));
setEditedMarkdown(null);
toast.success("Document saved! Reindexing in background...");
return true;
} catch (err) {
console.error("Error saving document:", err);
toast.error(err instanceof Error ? err.message : "Failed to save document");
return false;
} finally {
setSaving(false);
}
}, [documentId, electronAPI, isLocalFileMode, localFilePath, searchSpaceId]);
const isEditableType = editorDoc const isEditableType = editorDoc
? (editorRenderMode === "source_code" || ? (editorRenderMode === "source_code" ||
@ -383,9 +384,15 @@ export function EditorPanelContent({
)} )}
</> </>
)} )}
{!showEditingActions && !isLocalFileMode && editorDoc?.document_type && documentId && ( {!showEditingActions &&
<VersionHistoryButton documentId={documentId} documentType={editorDoc.document_type} /> !isLocalFileMode &&
)} editorDoc?.document_type &&
documentId && (
<VersionHistoryButton
documentId={documentId}
documentType={editorDoc.document_type}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@ -533,11 +540,7 @@ export function EditorPanelContent({
} }
}} }}
> >
{downloading ? ( {downloading ? <Spinner size="xs" /> : <Download className="size-3.5" />}
<Spinner size="xs" />
) : (
<Download className="size-3.5" />
)}
{downloading ? "Preparing..." : "Download .md"} {downloading ? "Preparing..." : "Download .md"}
</Button> </Button>
</AlertDescription> </AlertDescription>
@ -564,7 +567,7 @@ export function EditorPanelContent({
</div> </div>
) : isEditableType ? ( ) : isEditableType ? (
<PlateEditor <PlateEditor
key={`${isLocalFileMode ? localFilePath ?? "local-file" : documentId}-${isEditing ? "editing" : "viewing"}`} key={`${isLocalFileMode ? (localFilePath ?? "local-file") : documentId}-${isEditing ? "editing" : "viewing"}`}
preset="full" preset="full"
markdown={editorDoc.source_markdown} markdown={editorDoc.source_markdown}
onMarkdownChange={handleMarkdownChange} onMarkdownChange={handleMarkdownChange}
@ -684,7 +687,8 @@ export function MobileEditorPanel() {
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.searchSpaceId
: !!panelState.localFilePath; : !!panelState.localFilePath;
if (isDesktop || !panelState.isOpen || !hasTarget || panelState.kind === "local_file") return null; if (isDesktop || !panelState.isOpen || !hasTarget || panelState.kind === "local_file")
return null;
return <MobileEditorDrawer />; return <MobileEditorDrawer />;
} }

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { createPlatePlugin } from "platejs/react"; import { createPlatePlugin, useEditorReadOnly } from "platejs/react";
import { useEditorReadOnly } from "platejs/react";
import { useEditorSave } from "@/components/editor/editor-save-context"; import { useEditorSave } from "@/components/editor/editor-save-context";
import { FixedToolbar } from "@/components/ui/fixed-toolbar"; import { FixedToolbar } from "@/components/ui/fixed-toolbar";

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useRef } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useEffect, useRef } from "react";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { const MonacoEditor = dynamic(() => import("@monaco-editor/react"), {

View file

@ -63,9 +63,9 @@ const TAB_ITEMS = [
featured: true, featured: true,
}, },
{ {
title: "Extreme Assist", title: "Screen capture in chat",
description: description:
"Get inline writing suggestions powered by your knowledge base as you type in any app.", "Capture your screen and send it with your message so the AI can see what you see.",
src: "/homepage/hero_tutorial/extreme_assist.mp4", src: "/homepage/hero_tutorial/extreme_assist.mp4",
featured: true, featured: true,
}, },

View file

@ -72,9 +72,7 @@ export function RightPanelExpandButton() {
const reportOpen = reportState.isOpen && !!reportState.reportId; const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen = const editorOpen =
editorState.isOpen && editorState.isOpen &&
(editorState.kind === "document" (editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
? !!editorState.documentId
: !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen; const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen;
@ -116,9 +114,7 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
const reportOpen = reportState.isOpen && !!reportState.reportId; const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen = const editorOpen =
editorState.isOpen && editorState.isOpen &&
(editorState.kind === "document" (editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
? !!editorState.documentId
: !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave; const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
useEffect(() => { useEffect(() => {

View file

@ -7,8 +7,8 @@ import {
ChevronRight, ChevronRight,
FileText, FileText,
Folder, Folder,
FolderPlus,
FolderClock, FolderClock,
FolderPlus,
Laptop, Laptop,
Lock, Lock,
Paperclip, Paperclip,
@ -63,6 +63,7 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -71,7 +72,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
@ -525,7 +525,9 @@ function AuthenticatedDocumentsSidebar({
if (!electronAPI) return; if (!electronAPI) return;
const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[]; const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[];
const matched = watchedFolders.find((wf: WatchedFolderEntry) => wf.rootFolderId === folder.id); const matched = watchedFolders.find(
(wf: WatchedFolderEntry) => wf.rootFolderId === folder.id
);
if (!matched) { if (!matched) {
toast.error("This folder is not being watched"); toast.error("This folder is not being watched");
return; return;
@ -555,7 +557,9 @@ function AuthenticatedDocumentsSidebar({
if (!electronAPI) return; if (!electronAPI) return;
const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[]; const watchedFolders = (await electronAPI.getWatchedFolders()) as WatchedFolderEntry[];
const matched = watchedFolders.find((wf: WatchedFolderEntry) => wf.rootFolderId === folder.id); const matched = watchedFolders.find(
(wf: WatchedFolderEntry) => wf.rootFolderId === folder.id
);
if (!matched) { if (!matched) {
toast.error("This folder is not being watched"); toast.error("This folder is not being watched");
return; return;
@ -988,7 +992,8 @@ function AuthenticatedDocumentsSidebar({
}, [open, onOpenChange, isMobile, setRightPanelCollapsed]); }, [open, onOpenChange, isMobile, setRightPanelCollapsed]);
const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings; const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings;
const currentFilesystemTab = filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud"; const currentFilesystemTab =
filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud";
const cloudContent = ( const cloudContent = (
<> <>
@ -1401,8 +1406,8 @@ function AuthenticatedDocumentsSidebar({
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Trust this workspace?</AlertDialogTitle> <AlertDialogTitle>Trust this workspace?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Local mode can read and edit files inside the folders you select. Continue only if Local mode can read and edit files inside the folders you select. Continue only if you
you trust this workspace and its contents. trust this workspace and its contents.
</AlertDialogDescription> </AlertDialogDescription>
{pendingLocalPath && ( {pendingLocalPath && (
<AlertDialogDescription className="mt-1 whitespace-pre-wrap break-words font-mono text-xs"> <AlertDialogDescription className="mt-1 whitespace-pre-wrap break-words font-mono text-xs">

View file

@ -273,7 +273,10 @@ export function LocalFilesystemBrowser({
const mount = mountByRootKey.get(rootKey); const mount = mountByRootKey.get(rootKey);
if (!state || state.loading) { if (!state || state.loading) {
return ( return (
<div key={rootPath} className="flex h-16 items-center gap-2 px-3 text-sm text-muted-foreground"> <div
key={rootPath}
className="flex h-16 items-center gap-2 px-3 text-sm text-muted-foreground"
>
<Spinner size="sm" /> <Spinner size="sm" />
<span>Loading {getFolderDisplayName(rootPath)}...</span> <span>Loading {getFolderDisplayName(rootPath)}...</span>
</div> </div>
@ -281,7 +284,10 @@ export function LocalFilesystemBrowser({
} }
if (state.error) { if (state.error) {
return ( return (
<div key={rootPath} className="rounded-md border border-destructive/20 bg-destructive/5 p-3"> <div
key={rootPath}
className="rounded-md border border-destructive/20 bg-destructive/5 p-3"
>
<p className="text-sm font-medium text-destructive">Failed to load local folder</p> <p className="text-sm font-medium text-destructive">Failed to load local folder</p>
<p className="mt-1 text-xs text-muted-foreground">{state.error}</p> <p className="mt-1 text-xs text-muted-foreground">{state.error}</p>
</div> </div>

View file

@ -1,7 +1,16 @@
"use client"; "use client";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Brain, CircleUser, Globe, Keyboard, KeyRound, Monitor, ReceiptText, Sparkles } from "lucide-react"; import {
Brain,
CircleUser,
Globe,
Keyboard,
KeyRound,
Monitor,
ReceiptText,
Sparkles,
} from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo } from "react"; import { useMemo } from "react";
@ -53,9 +62,9 @@ const DesktopContent = dynamic(
); );
const DesktopShortcutsContent = dynamic( const DesktopShortcutsContent = dynamic(
() => () =>
import("@/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent").then( import(
(m) => ({ default: m.DesktopShortcutsContent }) "@/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent"
), ).then((m) => ({ default: m.DesktopShortcutsContent })),
{ ssr: false } { ssr: false }
); );
const MemoryContent = dynamic( const MemoryContent = dynamic(

View file

@ -118,7 +118,9 @@ function GenericApprovalCard({
setProcessing(); setProcessing();
onDecision({ type: "approve" }); onDecision({ type: "approve" });
connectorsApiService.trustMCPTool(mcpConnectorId, toolName).catch(() => { connectorsApiService.trustMCPTool(mcpConnectorId, toolName).catch(() => {
toast.error("Failed to save 'Always Allow' preference. The tool will still require approval next time."); toast.error(
"Failed to save 'Always Allow' preference. The tool will still require approval next time."
);
}); });
}, [phase, setProcessing, onDecision, isMCPTool, mcpConnectorId, toolName]); }, [phase, setProcessing, onDecision, isMCPTool, mcpConnectorId, toolName]);

View file

@ -2,7 +2,14 @@
import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import type { ToolCallMessagePartProps } from "@assistant-ui/react";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pencil, UsersIcon } from "lucide-react"; import {
ClockIcon,
CornerDownLeftIcon,
GlobeIcon,
MapPinIcon,
Pencil,
UsersIcon,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom"; import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";

View file

@ -1,8 +1,8 @@
import { import {
BookOpen, BookOpen,
Brain, Brain,
FileUser,
FileText, FileText,
FileUser,
Film, Film,
Globe, Globe,
ImageIcon, ImageIcon,