mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 01:02:39 +02:00
Indent tool cards under an active delegating task span.
This commit is contained in:
parent
39084b3075
commit
e7c5204b02
4 changed files with 106 additions and 52 deletions
|
|
@ -4,6 +4,7 @@ import {
|
||||||
AuiIf,
|
AuiIf,
|
||||||
ErrorPrimitive,
|
ErrorPrimitive,
|
||||||
MessagePrimitive,
|
MessagePrimitive,
|
||||||
|
type ToolCallMessagePartComponent,
|
||||||
useAui,
|
useAui,
|
||||||
useAuiState,
|
useAuiState,
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
|
|
@ -36,7 +37,7 @@ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||||
import { ReasoningMessagePart } from "@/components/assistant-ui/reasoning-message-part";
|
import { ReasoningMessagePart } from "@/components/assistant-ui/reasoning-message-part";
|
||||||
import { RevertTurnButton } from "@/components/assistant-ui/revert-turn-button";
|
import { RevertTurnButton } from "@/components/assistant-ui/revert-turn-button";
|
||||||
import { useTokenUsage } from "@/components/assistant-ui/token-usage-context";
|
import { useTokenUsage } from "@/components/assistant-ui/token-usage-context";
|
||||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
import { ToolFallback, withDelegationSpanIndent } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
||||||
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
||||||
|
|
@ -505,48 +506,55 @@ const MessageInfoDropdown: FC = () => {
|
||||||
|
|
||||||
// Wrap each tool-ui card with ``withBundleStep`` so multi-card HITL bundles
|
// Wrap each tool-ui card with ``withBundleStep`` so multi-card HITL bundles
|
||||||
// page through them and stage decisions instead of firing one resume per card.
|
// page through them and stage decisions instead of firing one resume per card.
|
||||||
|
// ``withDelegationSpanIndent`` wraps every entry (including Fallback) so delegated
|
||||||
|
// subagent tools don't bypass span indentation via a named ``by_name`` UI.
|
||||||
|
const bundleTool = (Component: ToolCallMessagePartComponent) =>
|
||||||
|
withBundleStep(withDelegationSpanIndent(Component));
|
||||||
|
|
||||||
|
const NullToolUi: ToolCallMessagePartComponent = () => null;
|
||||||
|
|
||||||
const TOOLS_BY_NAME = {
|
const TOOLS_BY_NAME = {
|
||||||
generate_report: withBundleStep(GenerateReportToolUI),
|
generate_report: bundleTool(GenerateReportToolUI),
|
||||||
generate_resume: withBundleStep(GenerateResumeToolUI),
|
generate_resume: bundleTool(GenerateResumeToolUI),
|
||||||
generate_podcast: withBundleStep(GeneratePodcastToolUI),
|
generate_podcast: bundleTool(GeneratePodcastToolUI),
|
||||||
generate_video_presentation: withBundleStep(GenerateVideoPresentationToolUI),
|
generate_video_presentation: bundleTool(GenerateVideoPresentationToolUI),
|
||||||
display_image: withBundleStep(GenerateImageToolUI),
|
display_image: bundleTool(GenerateImageToolUI),
|
||||||
generate_image: withBundleStep(GenerateImageToolUI),
|
generate_image: bundleTool(GenerateImageToolUI),
|
||||||
update_memory: withBundleStep(UpdateMemoryToolUI),
|
update_memory: bundleTool(UpdateMemoryToolUI),
|
||||||
execute: withBundleStep(SandboxExecuteToolUI),
|
execute: bundleTool(SandboxExecuteToolUI),
|
||||||
execute_code: withBundleStep(SandboxExecuteToolUI),
|
execute_code: bundleTool(SandboxExecuteToolUI),
|
||||||
create_notion_page: withBundleStep(CreateNotionPageToolUI),
|
create_notion_page: bundleTool(CreateNotionPageToolUI),
|
||||||
update_notion_page: withBundleStep(UpdateNotionPageToolUI),
|
update_notion_page: bundleTool(UpdateNotionPageToolUI),
|
||||||
delete_notion_page: withBundleStep(DeleteNotionPageToolUI),
|
delete_notion_page: bundleTool(DeleteNotionPageToolUI),
|
||||||
create_linear_issue: withBundleStep(CreateLinearIssueToolUI),
|
create_linear_issue: bundleTool(CreateLinearIssueToolUI),
|
||||||
update_linear_issue: withBundleStep(UpdateLinearIssueToolUI),
|
update_linear_issue: bundleTool(UpdateLinearIssueToolUI),
|
||||||
delete_linear_issue: withBundleStep(DeleteLinearIssueToolUI),
|
delete_linear_issue: bundleTool(DeleteLinearIssueToolUI),
|
||||||
create_google_drive_file: withBundleStep(CreateGoogleDriveFileToolUI),
|
create_google_drive_file: bundleTool(CreateGoogleDriveFileToolUI),
|
||||||
delete_google_drive_file: withBundleStep(DeleteGoogleDriveFileToolUI),
|
delete_google_drive_file: bundleTool(DeleteGoogleDriveFileToolUI),
|
||||||
create_onedrive_file: withBundleStep(CreateOneDriveFileToolUI),
|
create_onedrive_file: bundleTool(CreateOneDriveFileToolUI),
|
||||||
delete_onedrive_file: withBundleStep(DeleteOneDriveFileToolUI),
|
delete_onedrive_file: bundleTool(DeleteOneDriveFileToolUI),
|
||||||
create_dropbox_file: withBundleStep(CreateDropboxFileToolUI),
|
create_dropbox_file: bundleTool(CreateDropboxFileToolUI),
|
||||||
delete_dropbox_file: withBundleStep(DeleteDropboxFileToolUI),
|
delete_dropbox_file: bundleTool(DeleteDropboxFileToolUI),
|
||||||
create_calendar_event: withBundleStep(CreateCalendarEventToolUI),
|
create_calendar_event: bundleTool(CreateCalendarEventToolUI),
|
||||||
update_calendar_event: withBundleStep(UpdateCalendarEventToolUI),
|
update_calendar_event: bundleTool(UpdateCalendarEventToolUI),
|
||||||
delete_calendar_event: withBundleStep(DeleteCalendarEventToolUI),
|
delete_calendar_event: bundleTool(DeleteCalendarEventToolUI),
|
||||||
create_gmail_draft: withBundleStep(CreateGmailDraftToolUI),
|
create_gmail_draft: bundleTool(CreateGmailDraftToolUI),
|
||||||
update_gmail_draft: withBundleStep(UpdateGmailDraftToolUI),
|
update_gmail_draft: bundleTool(UpdateGmailDraftToolUI),
|
||||||
send_gmail_email: withBundleStep(SendGmailEmailToolUI),
|
send_gmail_email: bundleTool(SendGmailEmailToolUI),
|
||||||
trash_gmail_email: withBundleStep(TrashGmailEmailToolUI),
|
trash_gmail_email: bundleTool(TrashGmailEmailToolUI),
|
||||||
create_jira_issue: withBundleStep(CreateJiraIssueToolUI),
|
create_jira_issue: bundleTool(CreateJiraIssueToolUI),
|
||||||
update_jira_issue: withBundleStep(UpdateJiraIssueToolUI),
|
update_jira_issue: bundleTool(UpdateJiraIssueToolUI),
|
||||||
delete_jira_issue: withBundleStep(DeleteJiraIssueToolUI),
|
delete_jira_issue: bundleTool(DeleteJiraIssueToolUI),
|
||||||
create_confluence_page: withBundleStep(CreateConfluencePageToolUI),
|
create_confluence_page: bundleTool(CreateConfluencePageToolUI),
|
||||||
update_confluence_page: withBundleStep(UpdateConfluencePageToolUI),
|
update_confluence_page: bundleTool(UpdateConfluencePageToolUI),
|
||||||
delete_confluence_page: withBundleStep(DeleteConfluencePageToolUI),
|
delete_confluence_page: bundleTool(DeleteConfluencePageToolUI),
|
||||||
web_search: () => null,
|
web_search: NullToolUi,
|
||||||
link_preview: () => null,
|
link_preview: NullToolUi,
|
||||||
multi_link_preview: () => null,
|
multi_link_preview: NullToolUi,
|
||||||
scrape_webpage: () => null,
|
scrape_webpage: NullToolUi,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const TOOLS_FALLBACK = withBundleStep(ToolFallback);
|
const TOOLS_FALLBACK = bundleTool(ToolFallback);
|
||||||
|
|
||||||
const AssistantMessageInner: FC = () => {
|
const AssistantMessageInner: FC = () => {
|
||||||
const isMobile = !useMediaQuery("(min-width: 768px)");
|
const isMobile = !useMediaQuery("(min-width: 768px)");
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ import { Spinner } from "@/components/ui/spinner";
|
||||||
import { getToolDisplayName } from "@/contracts/enums/toolIcons";
|
import { getToolDisplayName } from "@/contracts/enums/toolIcons";
|
||||||
import { markActionRevertedInCache, useAgentActionsQuery } from "@/hooks/use-agent-actions-query";
|
import { markActionRevertedInCache, useAgentActionsQuery } from "@/hooks/use-agent-actions-query";
|
||||||
import { agentActionsApiService } from "@/lib/apis/agent-actions-api.service";
|
import { agentActionsApiService } from "@/lib/apis/agent-actions-api.service";
|
||||||
|
import {
|
||||||
|
DELEGATION_SPAN_INDENT_CLASS,
|
||||||
|
shouldIndentToolCallForDelegationSpan,
|
||||||
|
} from "@/lib/chat/delegation-span-indent";
|
||||||
import { AppError } from "@/lib/error";
|
import { AppError } from "@/lib/error";
|
||||||
import { isInterruptResult } from "@/lib/hitl";
|
import { isInterruptResult } from "@/lib/hitl";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -499,6 +503,24 @@ const DefaultToolFallbackInner: ToolCallMessagePartComponent = (props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap any tool-call UI so cards under an active delegating ``task`` span indent.
|
||||||
|
* Applied to named tool components as well as ``ToolFallback`` — only ``ToolFallback``
|
||||||
|
* would miss delegated tools otherwise.
|
||||||
|
*/
|
||||||
|
export function withDelegationSpanIndent(
|
||||||
|
Component: ToolCallMessagePartComponent
|
||||||
|
): ToolCallMessagePartComponent {
|
||||||
|
const Wrapped: ToolCallMessagePartComponent = (props) => {
|
||||||
|
const metadata = (props as { metadata?: Record<string, unknown> }).metadata;
|
||||||
|
const indent = shouldIndentToolCallForDelegationSpan(props.toolName, metadata);
|
||||||
|
const inner = <Component {...props} />;
|
||||||
|
return indent ? <div className={cn(DELEGATION_SPAN_INDENT_CLASS)}>{inner}</div> : inner;
|
||||||
|
};
|
||||||
|
Wrapped.displayName = `withDelegationSpanIndent(${Component.displayName ?? Component.name ?? "ToolUI"})`;
|
||||||
|
return Wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
export const ToolFallback: ToolCallMessagePartComponent = (props) => {
|
export const ToolFallback: ToolCallMessagePartComponent = (props) => {
|
||||||
if (isInterruptResult(props.result)) {
|
if (isInterruptResult(props.result)) {
|
||||||
if (isDoomLoopInterrupt(props.result)) {
|
if (isDoomLoopInterrupt(props.result)) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
AuiIf,
|
AuiIf,
|
||||||
MessagePrimitive,
|
MessagePrimitive,
|
||||||
ThreadPrimitive,
|
ThreadPrimitive,
|
||||||
|
type ToolCallMessagePartComponent,
|
||||||
useAuiState,
|
useAuiState,
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
|
|
@ -14,7 +15,7 @@ import { type FC, type ReactNode, useState } from "react";
|
||||||
import { CitationMetadataProvider } from "@/components/assistant-ui/citation-metadata-context";
|
import { CitationMetadataProvider } from "@/components/assistant-ui/citation-metadata-context";
|
||||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||||
import { ReasoningMessagePart } from "@/components/assistant-ui/reasoning-message-part";
|
import { ReasoningMessagePart } from "@/components/assistant-ui/reasoning-message-part";
|
||||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
import { ToolFallback, withDelegationSpanIndent } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
|
import { GenerateImageToolUI } from "@/components/tool-ui/generate-image";
|
||||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||||
|
|
@ -29,6 +30,8 @@ const GenerateVideoPresentationToolUI = dynamic(
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const NullToolUi: ToolCallMessagePartComponent = () => null;
|
||||||
|
|
||||||
interface PublicThreadProps {
|
interface PublicThreadProps {
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
@ -162,18 +165,20 @@ const PublicAssistantMessage: FC = () => {
|
||||||
Reasoning: ReasoningMessagePart,
|
Reasoning: ReasoningMessagePart,
|
||||||
tools: {
|
tools: {
|
||||||
by_name: {
|
by_name: {
|
||||||
generate_podcast: GeneratePodcastToolUI,
|
generate_podcast: withDelegationSpanIndent(GeneratePodcastToolUI),
|
||||||
generate_report: GenerateReportToolUI,
|
generate_report: withDelegationSpanIndent(GenerateReportToolUI),
|
||||||
generate_resume: GenerateResumeToolUI,
|
generate_resume: withDelegationSpanIndent(GenerateResumeToolUI),
|
||||||
generate_video_presentation: GenerateVideoPresentationToolUI,
|
generate_video_presentation: withDelegationSpanIndent(
|
||||||
display_image: GenerateImageToolUI,
|
GenerateVideoPresentationToolUI
|
||||||
generate_image: GenerateImageToolUI,
|
),
|
||||||
web_search: () => null,
|
display_image: withDelegationSpanIndent(GenerateImageToolUI),
|
||||||
link_preview: () => null,
|
generate_image: withDelegationSpanIndent(GenerateImageToolUI),
|
||||||
multi_link_preview: () => null,
|
web_search: NullToolUi,
|
||||||
scrape_webpage: () => null,
|
link_preview: NullToolUi,
|
||||||
|
multi_link_preview: NullToolUi,
|
||||||
|
scrape_webpage: NullToolUi,
|
||||||
},
|
},
|
||||||
Fallback: ToolFallback,
|
Fallback: withDelegationSpanIndent(ToolFallback),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
19
surfsense_web/lib/chat/delegation-span-indent.ts
Normal file
19
surfsense_web/lib/chat/delegation-span-indent.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Indent tool-call cards that belong to an open delegating ``task`` episode.
|
||||||
|
*
|
||||||
|
* The backend only stamps ``metadata.spanId`` on tool SSE / persisted parts
|
||||||
|
* while a ``task`` is active (see ``AgentEventRelayState.tool_activity_metadata``),
|
||||||
|
* so its presence is sufficient. The opening ``task`` row itself carries the
|
||||||
|
* same span id but stays flush — it is the header of the delegation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function shouldIndentToolCallForDelegationSpan(
|
||||||
|
toolName: string,
|
||||||
|
metadata: Record<string, unknown> | undefined
|
||||||
|
): boolean {
|
||||||
|
if (toolName === "task") return false;
|
||||||
|
const v = metadata?.spanId;
|
||||||
|
return typeof v === "string" && v.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELEGATION_SPAN_INDENT_CLASS = "pl-3 sm:ml-4";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue