feat: render presentation chat with assistant ui
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||
import { AssistantOperatorThread } from "./AssistantOperatorThread.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("AssistantOperatorThread", () => {
|
||||
it("renders text interleaved with a collapsed tool call", async () => {
|
||||
const user = userEvent.setup();
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "text", text: "I will inspect the run." },
|
||||
{
|
||||
type: "tool-call",
|
||||
call: { id: "call-1", name: "readRunTrace", input: { run_id: "run_1" } },
|
||||
},
|
||||
{ type: "text", text: "The trace is inspectable." },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||
|
||||
expect(screen.getByRole("log", { name: /operator conversation/i })).toBeInTheDocument();
|
||||
expect(screen.getByText("I will inspect the run.")).toBeInTheDocument();
|
||||
expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument();
|
||||
const tool = screen.getByRole("button", { name: /readRunTrace/i });
|
||||
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument();
|
||||
await user.click(tool);
|
||||
expect(screen.getByText(/run_1/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders grouped consecutive tool calls", () => {
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "assistant-tools",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "tool-call", call: { id: "call-1", name: "readRunTrace", input: {} } },
|
||||
{ type: "tool-call", call: { id: "call-2", name: "inspectDeployment", input: { deployment_id: "demo" } } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||
|
||||
expect(screen.getByText(/2 tools/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /readRunTrace/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /inspectDeployment/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders schema approval through the existing approval surface", async () => {
|
||||
const user = userEvent.setup();
|
||||
const submit = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "approval",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{
|
||||
type: "approval-request",
|
||||
callId: "call-approval",
|
||||
name: "resumeIssueReview",
|
||||
prompt: "Submit resume request?",
|
||||
contract: {
|
||||
kind: "issue_review",
|
||||
outcomes: ["submitted", "cancelled"],
|
||||
resumeSchema: { type: "object" },
|
||||
resumePayloadPreview: { selected_issue_ids: ["risk-1"] },
|
||||
runId: "run_recorded_lda_report",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<AssistantOperatorThread
|
||||
mode="dock"
|
||||
messages={messages}
|
||||
submitApproval={submit}
|
||||
cancelApproval={cancel}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("group", { name: /issue review resume/i })).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Submit" }));
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(submit).toHaveBeenCalledOnce();
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("renders a chat-owned run action", async () => {
|
||||
const user = userEvent.setup();
|
||||
const run = vi.fn();
|
||||
render(
|
||||
<AssistantOperatorThread
|
||||
mode="dock"
|
||||
messages={[]}
|
||||
runAction={{ label: "Run prepared workflow", disabled: false, run }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /run prepared workflow/i }));
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
AssistantRuntimeProvider,
|
||||
MessagePrimitive,
|
||||
ThreadPrimitive,
|
||||
useExternalStoreRuntime,
|
||||
type AppendMessage,
|
||||
} from "@assistant-ui/react";
|
||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||
import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js";
|
||||
import { projectAgentMessagesForAssistant, type AssistantProjectedMessage } from "./assistantRuntimeProjection.js";
|
||||
|
||||
type AssistantOperatorThreadProps = {
|
||||
readonly mode: "hidden" | "full" | "rail" | "dock";
|
||||
readonly messages: ReadonlyArray<AgentMessage>;
|
||||
readonly runAction?: { readonly label: string; readonly disabled: boolean; readonly run: () => void } | undefined;
|
||||
readonly submitApproval?: (() => void) | undefined;
|
||||
readonly cancelApproval?: (() => void) | undefined;
|
||||
readonly ariaLabel?: string | undefined;
|
||||
};
|
||||
|
||||
const formatJson = (value: unknown): string => {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const ToolCard = ({
|
||||
toolName,
|
||||
args,
|
||||
result,
|
||||
isError,
|
||||
}: {
|
||||
readonly toolName: string;
|
||||
readonly args?: unknown;
|
||||
readonly result?: unknown;
|
||||
readonly isError?: boolean;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div className="assistant-tool-card" data-open={open ? "true" : "false"}>
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-tool-card__trigger"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((c) => !c)}
|
||||
>
|
||||
<span className="assistant-tool-card__label">Used tool: <b>{toolName}</b></span>
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="assistant-tool-card__body">
|
||||
{args !== undefined ? (
|
||||
<pre className="assistant-tool-card__io" aria-label="tool input">{formatJson(args)}</pre>
|
||||
) : null}
|
||||
{result !== undefined ? (
|
||||
<div className="assistant-tool-card__result" data-error={isError ? "true" : undefined}>
|
||||
<span>{isError ? "error" : "success"}</span>
|
||||
<pre className="assistant-tool-card__io" aria-label="tool output">{formatJson(result)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ToolGroupCard = ({
|
||||
toolCount,
|
||||
defaultOpen = true,
|
||||
children,
|
||||
}: {
|
||||
readonly toolCount: number;
|
||||
readonly defaultOpen?: boolean;
|
||||
readonly children: React.ReactNode;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return (
|
||||
<div className="assistant-tool-group" data-open={open ? "true" : "false"}>
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-tool-group__trigger"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((c) => !c)}
|
||||
>
|
||||
<span>{toolCount} tools</span>
|
||||
</button>
|
||||
{open ? <div className="assistant-tool-group__body">{children}</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentPart =
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "tool-call"; readonly toolName: string; readonly args?: unknown; readonly result?: unknown; readonly isError?: boolean };
|
||||
|
||||
const renderContentPart = (
|
||||
part: ContentPart,
|
||||
submitApproval?: (() => void) | undefined,
|
||||
cancelApproval?: (() => void) | undefined,
|
||||
): React.ReactNode => {
|
||||
if (part.type === "text") {
|
||||
return <MessagePrimitive.Content />;
|
||||
}
|
||||
const toolName = part.toolName;
|
||||
const args = part.args as Record<string, unknown> | undefined;
|
||||
const contract = args?.contract as
|
||||
| {
|
||||
readonly kind: string;
|
||||
readonly outcomes: readonly string[];
|
||||
readonly resumeSchema: unknown;
|
||||
readonly resumePayloadPreview: unknown;
|
||||
readonly runId: string;
|
||||
}
|
||||
| undefined;
|
||||
if (toolName === "resumeIssueReview" && contract) {
|
||||
return (
|
||||
<div className="assistant-tool-approval">
|
||||
<SchemaApprovalSurface
|
||||
title={`${contract.kind.replaceAll("_", " ")} resume`}
|
||||
schema={contract.resumeSchema}
|
||||
payload={contract.resumePayloadPreview}
|
||||
outcomes={contract.outcomes}
|
||||
runId={contract.runId}
|
||||
onSubmit={submitApproval}
|
||||
onCancel={cancelApproval}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <ToolCard toolName={toolName} args={part.args} result={part.result} isError={part.isError ?? false} />;
|
||||
};
|
||||
|
||||
const AssistantMessageBody = ({
|
||||
parts,
|
||||
submitApproval,
|
||||
cancelApproval,
|
||||
}: {
|
||||
readonly parts: readonly ContentPart[];
|
||||
readonly submitApproval?: (() => void) | undefined;
|
||||
readonly cancelApproval?: (() => void) | undefined;
|
||||
}) => {
|
||||
const textParts = parts.filter((p) => p.type === "text");
|
||||
const toolParts = parts.filter((p) => p.type === "tool-call");
|
||||
|
||||
if (toolParts.length === 0) {
|
||||
return <MessagePrimitive.Content />;
|
||||
}
|
||||
|
||||
if (toolParts.length === 1) {
|
||||
const firstToolIndex = parts.findIndex((p) => p.type === "tool-call");
|
||||
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
|
||||
const afterText = parts.slice(firstToolIndex + 1).filter((p) => p.type === "text");
|
||||
const tool = toolParts[0]!;
|
||||
return (
|
||||
<>
|
||||
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||
{renderContentPart(tool, submitApproval, cancelApproval)}
|
||||
{afterText.map((p, i) => <p key={`after-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const firstToolIndex = parts.findIndex((p) => p.type === "tool-call");
|
||||
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
|
||||
return (
|
||||
<>
|
||||
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||
<ToolGroupCard toolCount={toolParts.length}>
|
||||
{toolParts.map((tool, i) => (
|
||||
<div key={i}>
|
||||
{renderContentPart(tool, submitApproval, cancelApproval)}
|
||||
</div>
|
||||
))}
|
||||
</ToolGroupCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadBody = ({
|
||||
submitApproval,
|
||||
cancelApproval,
|
||||
}: {
|
||||
readonly submitApproval?: (() => void) | undefined;
|
||||
readonly cancelApproval?: (() => void) | undefined;
|
||||
}) => (
|
||||
<ThreadPrimitive.Root className="assistant-thread">
|
||||
<ThreadPrimitive.Viewport className="assistant-thread__viewport">
|
||||
<ThreadPrimitive.Messages>
|
||||
{({ message }) => {
|
||||
const content = (message as unknown as { content?: unknown }).content;
|
||||
const parts: ContentPart[] = Array.isArray(content)
|
||||
? (content as ContentPart[])
|
||||
: typeof content === "string"
|
||||
? [{ type: "text" as const, text: content }]
|
||||
: [];
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className="assistant-message"
|
||||
data-role={message.role}
|
||||
>
|
||||
<AssistantMessageBody
|
||||
parts={parts}
|
||||
submitApproval={submitApproval}
|
||||
cancelApproval={cancelApproval}
|
||||
/>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
}}
|
||||
</ThreadPrimitive.Messages>
|
||||
</ThreadPrimitive.Viewport>
|
||||
</ThreadPrimitive.Root>
|
||||
);
|
||||
|
||||
export const AssistantOperatorThread = ({
|
||||
mode,
|
||||
messages,
|
||||
runAction,
|
||||
submitApproval,
|
||||
cancelApproval,
|
||||
ariaLabel = "operator conversation",
|
||||
}: AssistantOperatorThreadProps) => {
|
||||
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
|
||||
const [localMessages, setLocalMessages] = useState<AssistantProjectedMessage[]>([]);
|
||||
const runtimeMessages = projected.length > 0 ? projected : localMessages;
|
||||
|
||||
const onNew = useCallback(async (message: AppendMessage) => {
|
||||
const text = message.content.find((part) => part.type === "text")?.text ?? "";
|
||||
setLocalMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
id: `local-${current.length + 1}`,
|
||||
role: "user" as const,
|
||||
content: [{ type: "text" as const, text }],
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const runtime = useExternalStoreRuntime({
|
||||
messages: runtimeMessages,
|
||||
setMessages: (msgs) => setLocalMessages([...msgs]),
|
||||
onNew,
|
||||
convertMessage: (message) => message,
|
||||
isRunning: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="assistant-operator-thread" data-mode={mode} role="log" aria-label={ariaLabel}>
|
||||
{runAction ? (
|
||||
<div className="assistant-operator-thread__action">
|
||||
<button type="button" disabled={runAction.disabled} onClick={runAction.run}>
|
||||
{runAction.label}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<ThreadBody submitApproval={submitApproval} cancelApproval={cancelApproval} />
|
||||
</AssistantRuntimeProvider>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -36,7 +36,7 @@ describe("assistantRuntimeProjection", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects tool results as tool-role messages", () => {
|
||||
it("projects tool results as text summary", () => {
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "tool-result-message",
|
||||
@@ -59,15 +59,9 @@ describe("assistantRuntimeProjection", () => {
|
||||
|
||||
expect(projected[0]).toMatchObject({
|
||||
id: "tool-result-message",
|
||||
role: "tool",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "call-1",
|
||||
toolName: "readRunTrace",
|
||||
result: { frames: 3 },
|
||||
isError: false,
|
||||
},
|
||||
{ type: "text", text: "Result for readRunTrace: success" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,27 +8,22 @@ export type AssistantToolRenderPayload = {
|
||||
readonly isError?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON-serializable args for assistant-ui compatibility
|
||||
type JsonArgs = Record<string, any>;
|
||||
|
||||
type AssistantContentPart =
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| {
|
||||
readonly type: "tool-call";
|
||||
readonly toolCallId: string;
|
||||
readonly toolCallId?: string;
|
||||
readonly toolName: string;
|
||||
readonly args: unknown;
|
||||
}
|
||||
| {
|
||||
readonly type: "tool-result";
|
||||
readonly toolCallId: string;
|
||||
readonly toolName: string;
|
||||
readonly result: unknown;
|
||||
readonly isError: boolean;
|
||||
readonly args?: JsonArgs;
|
||||
};
|
||||
|
||||
export type AssistantProjectedMessage = {
|
||||
readonly id: string;
|
||||
readonly role: "user" | "assistant" | "tool";
|
||||
readonly role: "user" | "assistant";
|
||||
readonly content: ReadonlyArray<AssistantContentPart>;
|
||||
readonly metadata?: { readonly unstable_state?: string | undefined };
|
||||
};
|
||||
|
||||
const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
||||
@@ -40,22 +35,19 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
||||
type: "tool-call",
|
||||
toolCallId: part.call.id,
|
||||
toolName: part.call.name,
|
||||
args: part.call.input,
|
||||
args: part.call.input as JsonArgs,
|
||||
}];
|
||||
case "tool-result":
|
||||
return [{
|
||||
type: "tool-result",
|
||||
toolCallId: part.result.callId,
|
||||
toolName: part.result.name,
|
||||
result: part.result.output,
|
||||
isError: part.result.status === "failure",
|
||||
type: "text",
|
||||
text: `Result for ${part.result.name}: ${part.result.status === "failure" ? "error" : "success"}`,
|
||||
}];
|
||||
case "presentation-action":
|
||||
return [{
|
||||
type: "tool-call",
|
||||
toolCallId: `presentation-${part.action.type}`,
|
||||
toolName: `presentation.${part.action.type}`,
|
||||
args: part.action,
|
||||
args: part.action as JsonArgs,
|
||||
}];
|
||||
case "approval-request":
|
||||
return [
|
||||
@@ -67,7 +59,7 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
||||
args: {
|
||||
prompt: part.prompt,
|
||||
contract: part.contract,
|
||||
},
|
||||
} as JsonArgs,
|
||||
},
|
||||
];
|
||||
case "error":
|
||||
@@ -76,9 +68,6 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
||||
};
|
||||
|
||||
const messageRoleFor = (message: AgentMessage): AssistantProjectedMessage["role"] => {
|
||||
const onlyToolResults = message.parts.length > 0
|
||||
&& message.parts.every((part) => part.type === "tool-result");
|
||||
if (onlyToolResults) return "tool";
|
||||
return message.role === "user" ? "user" : "assistant";
|
||||
};
|
||||
|
||||
|
||||
@@ -1638,6 +1638,145 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.assistant-operator-thread {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 0.55rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.assistant-operator-thread[data-mode="hidden"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assistant-operator-thread__action button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--accent-cyan);
|
||||
border-radius: 0.65rem;
|
||||
background: color-mix(in oklch, var(--accent-cyan) 13%, var(--stage-surface));
|
||||
color: var(--text-primary);
|
||||
padding: 0.55rem 0.7rem;
|
||||
font: 700 0.8rem/1 var(--font-interface);
|
||||
}
|
||||
|
||||
.assistant-thread,
|
||||
.assistant-thread__viewport {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.assistant-thread__viewport {
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.assistant-thread__viewport::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.assistant-tool-card {
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 0.65rem;
|
||||
background: var(--stage-inset);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.assistant-tool-card__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font: 600 0.78rem/1 var(--font-interface);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.assistant-tool-card__trigger:hover {
|
||||
background: color-mix(in oklch, var(--accent-cyan) 8%, transparent);
|
||||
}
|
||||
|
||||
.assistant-tool-card__body {
|
||||
padding: 0 0.65rem 0.55rem;
|
||||
}
|
||||
|
||||
.assistant-tool-card__io {
|
||||
background: var(--stage-surface);
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
font: 0.72rem/1.45 var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.assistant-tool-card__result {
|
||||
margin-top: 0.35rem;
|
||||
font: 600 0.7rem/1 var(--font-interface);
|
||||
}
|
||||
|
||||
.assistant-tool-card__result span {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.assistant-tool-card__result[data-error="true"] span {
|
||||
color: var(--accent-error);
|
||||
}
|
||||
|
||||
.assistant-tool-group {
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 0.65rem;
|
||||
background: var(--stage-inset);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.assistant-tool-group__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font: 600 0.78rem/1 var(--font-interface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assistant-tool-group__trigger:hover {
|
||||
background: color-mix(in oklch, var(--accent-cyan) 8%, transparent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.assistant-tool-group__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
padding: 0 0.65rem 0.55rem;
|
||||
}
|
||||
|
||||
.assistant-tool-group__body .assistant-tool-card {
|
||||
border-color: var(--stage-line);
|
||||
}
|
||||
|
||||
.assistant-tool-approval {
|
||||
border: 1px solid color-mix(in oklch, var(--accent-cyan) 50%, var(--stage-line));
|
||||
border-radius: 0.75rem;
|
||||
background: var(--stage-inset);
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.operator-chat .ai-chat-conversation {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
||||
@@ -28,3 +28,7 @@ if (!globalThis.DOMRect) {
|
||||
}),
|
||||
} as unknown as typeof DOMRect;
|
||||
}
|
||||
|
||||
if (!HTMLDivElement.prototype.scrollTo) {
|
||||
HTMLDivElement.prototype.scrollTo = () => {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user