fix: align assistant ui presentation chat
This commit is contained in:
@@ -7,7 +7,10 @@ export type WorkflowToolName =
|
|||||||
export type PresentationToolName =
|
export type PresentationToolName =
|
||||||
| "selectWorkflowNode"
|
| "selectWorkflowNode"
|
||||||
| "focusOperation"
|
| "focusOperation"
|
||||||
| "showTraceFrame";
|
| "showTraceFrame"
|
||||||
|
| "fetchData"
|
||||||
|
| "transformPayload"
|
||||||
|
| "writeOutput";
|
||||||
|
|
||||||
export type AgentToolName = WorkflowToolName | PresentationToolName;
|
export type AgentToolName = WorkflowToolName | PresentationToolName;
|
||||||
|
|
||||||
@@ -53,6 +56,21 @@ export const AGENT_TOOLS = {
|
|||||||
kind: "presentation",
|
kind: "presentation",
|
||||||
description: "Focus a trace frame in the presentation.",
|
description: "Focus a trace frame in the presentation.",
|
||||||
},
|
},
|
||||||
|
fetchData: {
|
||||||
|
name: "fetchData",
|
||||||
|
kind: "presentation",
|
||||||
|
description: "Presentation-only placeholder for fetching data in a one-off tool loop.",
|
||||||
|
},
|
||||||
|
transformPayload: {
|
||||||
|
name: "transformPayload",
|
||||||
|
kind: "presentation",
|
||||||
|
description: "Presentation-only placeholder for transforming data in a one-off tool loop.",
|
||||||
|
},
|
||||||
|
writeOutput: {
|
||||||
|
name: "writeOutput",
|
||||||
|
kind: "presentation",
|
||||||
|
description: "Presentation-only placeholder for writing output in a one-off tool loop.",
|
||||||
|
},
|
||||||
} satisfies Record<AgentToolName, AgentToolDescriptor>;
|
} satisfies Record<AgentToolName, AgentToolDescriptor>;
|
||||||
|
|
||||||
export const isAllowedAgentToolName = (name: string): name is AgentToolName =>
|
export const isAllowedAgentToolName = (name: string): name is AgentToolName =>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe("OperatorChat", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("Prepare the report.")).toBeInTheDocument();
|
expect(screen.getByText("Prepare the report.")).toBeInTheDocument();
|
||||||
expect(screen.getByText("I will use the prepared recipe.")).toBeInTheDocument();
|
expect(screen.getByText("I will use the prepared recipe.")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /selectWorkflowNode/i })).toBeInTheDocument();
|
expect(screen.getAllByRole("button", { name: /selectWorkflowNode/i }).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders tool calls as open tool cards", async () => {
|
it("renders tool calls as open tool cards", async () => {
|
||||||
@@ -151,16 +151,16 @@ describe("OperatorChat", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(<OperatorChat state={initialPresentationState} messages={messages} />);
|
const { container } = render(<OperatorChat state={initialPresentationState} messages={messages} />);
|
||||||
|
|
||||||
expect(screen.queryByRole("group", { name: /issue review resume/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("group", { name: /issue review resume/i })).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("Submit resume request?")).toBeInTheDocument();
|
expect(screen.getByText("Submit resume request?")).toBeInTheDocument();
|
||||||
const tool = screen.getByRole("button", { name: /resumeIssueReview/i });
|
const tool = screen.getByRole("button", { name: /resumeIssueReview/i });
|
||||||
expect(screen.queryByRole("button", { name: /Approve/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: /Approve/i })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: /Deny/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: /Deny/i })).not.toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("tool input")).toBeInTheDocument();
|
expect(container.querySelector('[data-slot="tool-fallback-args"]')).toBeInTheDocument();
|
||||||
await user.click(tool);
|
await user.click(tool);
|
||||||
expect(screen.queryByLabelText("tool input")).not.toBeInTheDocument();
|
expect(container.querySelector('[data-slot="tool-fallback-args"]')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders error and presentation action parts", () => {
|
it("renders error and presentation action parts", () => {
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ describe("AssistantOperatorThread", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
const { container } = render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||||
|
|
||||||
expect(screen.getByRole("log", { name: /operator conversation/i })).toBeInTheDocument();
|
expect(screen.getByRole("log", { name: /operator conversation/i })).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-slot="tool-fallback-root"]')).toBeInTheDocument();
|
||||||
expect(screen.getByText("I will inspect the run.")).toBeInTheDocument();
|
expect(screen.getByText("I will inspect the run.")).toBeInTheDocument();
|
||||||
expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument();
|
expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument();
|
||||||
const tool = screen.getByRole("button", { name: /readRunTrace/i });
|
const tool = screen.getByRole("button", { name: /readRunTrace/i });
|
||||||
@@ -48,9 +49,10 @@ describe("AssistantOperatorThread", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
const { container } = render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||||
|
|
||||||
expect(screen.getByText(/2 tools/i)).toBeInTheDocument();
|
expect(container.querySelector('[data-slot="tool-group-root"]')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/2 tool calls/i)).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /readRunTrace/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /readRunTrace/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /inspectDeployment/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /inspectDeployment/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -111,4 +113,29 @@ describe("AssistantOperatorThread", () => {
|
|||||||
await user.click(screen.getByRole("button", { name: /run prepared workflow/i }));
|
await user.click(screen.getByRole("button", { name: /run prepared workflow/i }));
|
||||||
expect(run).toHaveBeenCalledOnce();
|
expect(run).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders structured tool results through the generated fallback result slot", () => {
|
||||||
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
|
{
|
||||||
|
id: "assistant-result",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
result: {
|
||||||
|
callId: "call-1",
|
||||||
|
name: "readRunTrace",
|
||||||
|
status: "success",
|
||||||
|
output: { frames: 3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { container } = render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||||
|
|
||||||
|
expect(container.querySelector('[data-slot="tool-fallback-result"]')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/"frames": 3/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, type ReactNode } from "react";
|
||||||
|
import type { ToolCallMessagePartStatus } from "@assistant-ui/react";
|
||||||
|
import {
|
||||||
|
ToolFallbackArgs,
|
||||||
|
ToolFallbackContent,
|
||||||
|
ToolFallbackError,
|
||||||
|
ToolFallbackResult,
|
||||||
|
ToolFallbackRoot,
|
||||||
|
ToolFallbackTrigger,
|
||||||
|
} from "../../components/assistant-ui/tool-fallback.js";
|
||||||
|
import {
|
||||||
|
ToolGroupContent,
|
||||||
|
ToolGroupRoot,
|
||||||
|
ToolGroupTrigger,
|
||||||
|
} from "../../components/assistant-ui/tool-group.js";
|
||||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js";
|
import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js";
|
||||||
import { projectAgentMessagesForAssistant, type AssistantProjectedMessage } from "./assistantRuntimeProjection.js";
|
import {
|
||||||
|
projectAgentMessagesForAssistant,
|
||||||
|
type AssistantContentPart,
|
||||||
|
} from "./assistantRuntimeProjection.js";
|
||||||
|
|
||||||
type AssistantOperatorThreadProps = {
|
type AssistantOperatorThreadProps = {
|
||||||
readonly mode: "hidden" | "full" | "rail" | "dock";
|
readonly mode: "hidden" | "full" | "rail" | "dock";
|
||||||
@@ -21,102 +38,97 @@ const formatJson = (value: unknown): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToolCard = ({
|
type ToolRenderPart = Extract<AssistantContentPart, { readonly type: "tool-call" | "tool-result" }>;
|
||||||
toolName,
|
|
||||||
args,
|
type ApprovalContract = {
|
||||||
defaultOpen = true,
|
readonly kind: string;
|
||||||
}: {
|
readonly outcomes: readonly string[];
|
||||||
readonly toolName: string;
|
readonly resumeSchema: unknown;
|
||||||
readonly args?: unknown;
|
readonly resumePayloadPreview: unknown;
|
||||||
readonly defaultOpen?: boolean;
|
readonly runId: string | null;
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
|
||||||
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">{toolName}</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}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToolGroupCard = ({
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
toolCount,
|
typeof value === "object" && value !== null;
|
||||||
defaultOpen = true,
|
|
||||||
children,
|
const approvalContractFromArgs = (args: unknown): ApprovalContract | undefined => {
|
||||||
}: {
|
if (!isRecord(args) || !isRecord(args.contract)) return undefined;
|
||||||
readonly toolCount: number;
|
const contract = args.contract;
|
||||||
readonly defaultOpen?: boolean;
|
if (
|
||||||
readonly children: React.ReactNode;
|
typeof contract.kind !== "string" ||
|
||||||
}) => {
|
!Array.isArray(contract.outcomes) ||
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
!contract.outcomes.every((outcome) => typeof outcome === "string") ||
|
||||||
return (
|
!("resumeSchema" in contract) ||
|
||||||
<div className="assistant-tool-group" data-open={open ? "true" : "false"}>
|
!("resumePayloadPreview" in contract) ||
|
||||||
<button
|
!(typeof contract.runId === "string" || contract.runId === null)
|
||||||
type="button"
|
) {
|
||||||
className="assistant-tool-group__trigger"
|
return undefined;
|
||||||
aria-expanded={open}
|
}
|
||||||
onClick={() => setOpen((c) => !c)}
|
return {
|
||||||
>
|
kind: contract.kind,
|
||||||
<span>{toolCount} tools</span>
|
outcomes: contract.outcomes,
|
||||||
</button>
|
resumeSchema: contract.resumeSchema,
|
||||||
{open ? <div className="assistant-tool-group__body">{children}</div> : null}
|
resumePayloadPreview: contract.resumePayloadPreview,
|
||||||
</div>
|
runId: contract.runId,
|
||||||
);
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContentPart =
|
const statusForToolPart = (part: ToolRenderPart): ToolCallMessagePartStatus | undefined => {
|
||||||
| { readonly type: "text"; readonly text: string }
|
if (part.type !== "tool-result") return undefined;
|
||||||
| { readonly type: "tool-call"; readonly toolName: string; readonly args?: unknown; readonly result?: unknown; readonly isError?: boolean };
|
if (part.status === "success") return { type: "complete" };
|
||||||
|
return { type: "incomplete", reason: "error", error: part.result };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultForToolPart = (part: ToolRenderPart): unknown =>
|
||||||
|
part.type === "tool-result" ? part.result : undefined;
|
||||||
|
|
||||||
const renderContentPart = (
|
const renderContentPart = (
|
||||||
part: ContentPart,
|
part: AssistantContentPart,
|
||||||
submitApproval?: (() => void) | undefined,
|
submitApproval?: (() => void) | undefined,
|
||||||
cancelApproval?: (() => void) | undefined,
|
cancelApproval?: (() => void) | undefined,
|
||||||
defaultOpen?: boolean,
|
defaultOpen?: boolean,
|
||||||
): React.ReactNode => {
|
): ReactNode => {
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
return <p style={{ whiteSpace: "pre-line" }}>{part.text}</p>;
|
return <p style={{ whiteSpace: "pre-line" }}>{part.text}</p>;
|
||||||
}
|
}
|
||||||
|
const status = statusForToolPart(part);
|
||||||
const toolName = part.toolName;
|
const toolName = part.toolName;
|
||||||
const args = part.args as Record<string, unknown> | undefined;
|
const args = part.type === "tool-call" ? part.args : undefined;
|
||||||
const contract = args?.contract as
|
const contract = part.type === "tool-call" ? approvalContractFromArgs(args) : undefined;
|
||||||
| {
|
const argsText = args !== undefined ? formatJson(args) : undefined;
|
||||||
readonly kind: string;
|
const result = resultForToolPart(part);
|
||||||
readonly outcomes: readonly string[];
|
|
||||||
readonly resumeSchema: unknown;
|
|
||||||
readonly resumePayloadPreview: unknown;
|
|
||||||
readonly runId: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
if (toolName === "resumeIssueReview" && contract) {
|
if (toolName === "resumeIssueReview" && contract) {
|
||||||
return (
|
return (
|
||||||
<div className="assistant-tool-approval">
|
<ToolFallbackRoot defaultOpen={defaultOpen ?? true}>
|
||||||
<SchemaApprovalSurface
|
<ToolFallbackTrigger toolName={toolName} status={{ type: "requires-action", reason: "interrupt" }} />
|
||||||
title={`${contract.kind.replaceAll("_", " ")} resume`}
|
<ToolFallbackContent>
|
||||||
schema={contract.resumeSchema}
|
{argsText !== undefined ? <ToolFallbackArgs argsText={argsText} /> : null}
|
||||||
payload={contract.resumePayloadPreview}
|
<div className="assistant-tool-approval">
|
||||||
outcomes={contract.outcomes}
|
<SchemaApprovalSurface
|
||||||
runId={contract.runId}
|
title={`${contract.kind.replaceAll("_", " ")} resume`}
|
||||||
onSubmit={submitApproval}
|
schema={contract.resumeSchema}
|
||||||
onCancel={cancelApproval}
|
payload={contract.resumePayloadPreview}
|
||||||
/>
|
outcomes={contract.outcomes}
|
||||||
</div>
|
runId={contract.runId}
|
||||||
|
onSubmit={submitApproval}
|
||||||
|
onCancel={cancelApproval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ToolFallbackContent>
|
||||||
|
</ToolFallbackRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ToolCard toolName={toolName} args={part.args} {...(defaultOpen !== undefined ? { defaultOpen } : {})} />;
|
return (
|
||||||
|
<ToolFallbackRoot defaultOpen={defaultOpen ?? true}>
|
||||||
|
<ToolFallbackTrigger toolName={toolName} {...(status !== undefined ? { status } : {})} />
|
||||||
|
<ToolFallbackContent>
|
||||||
|
<ToolFallbackError {...(status !== undefined ? { status } : {})} />
|
||||||
|
{argsText !== undefined ? <ToolFallbackArgs argsText={argsText} /> : null}
|
||||||
|
<ToolFallbackResult result={result} />
|
||||||
|
</ToolFallbackContent>
|
||||||
|
</ToolFallbackRoot>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AssistantMessageBody = ({
|
const AssistantMessageBody = ({
|
||||||
@@ -124,51 +136,56 @@ const AssistantMessageBody = ({
|
|||||||
submitApproval,
|
submitApproval,
|
||||||
cancelApproval,
|
cancelApproval,
|
||||||
}: {
|
}: {
|
||||||
readonly parts: readonly ContentPart[];
|
readonly parts: readonly AssistantContentPart[];
|
||||||
readonly submitApproval?: (() => void) | undefined;
|
readonly submitApproval?: (() => void) | undefined;
|
||||||
readonly cancelApproval?: (() => void) | undefined;
|
readonly cancelApproval?: (() => void) | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
const toolParts = parts.filter((p) => p.type === "tool-call");
|
const rendered: ReactNode[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
if (toolParts.length === 0) {
|
while (index < parts.length) {
|
||||||
return (
|
const part = parts[index]!;
|
||||||
<>
|
if (part.type === "text") {
|
||||||
{parts.filter((p) => p.type === "text").map((p, i) => (
|
rendered.push(
|
||||||
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
|
<p key={`text-${index}`} style={{ whiteSpace: "pre-line" }}>{part.text}</p>,
|
||||||
))}
|
);
|
||||||
</>
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolRunStart = index;
|
||||||
|
const toolRun: ToolRenderPart[] = [];
|
||||||
|
while (index < parts.length && parts[index]!.type !== "text") {
|
||||||
|
toolRun.push(parts[index]! as ToolRenderPart);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolRun.length === 1) {
|
||||||
|
rendered.push(
|
||||||
|
<div key={`tool-${toolRunStart}`}>
|
||||||
|
{renderContentPart(toolRun[0]!, submitApproval, cancelApproval)}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered.push(
|
||||||
|
<ToolGroupRoot key={`tool-group-${toolRunStart}`} defaultOpen>
|
||||||
|
<ToolGroupTrigger count={toolRun.length} />
|
||||||
|
<ToolGroupContent>
|
||||||
|
{toolRun.map((tool, toolIndex) => (
|
||||||
|
<div key={`${tool.type}-${tool.toolName}-${tool.toolCallId ?? "no-id"}-${toolIndex}`}>
|
||||||
|
{renderContentPart(tool, submitApproval, cancelApproval, false)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ToolGroupContent>
|
||||||
|
</ToolGroupRoot>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 lastToolIndex = parts.findLastIndex((p) => p.type === "tool-call");
|
|
||||||
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
|
|
||||||
const afterText = parts.slice(lastToolIndex + 1).filter((p) => p.type === "text");
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
{rendered}
|
||||||
<ToolGroupCard toolCount={toolParts.length}>
|
|
||||||
{toolParts.map((tool, i) => (
|
|
||||||
<div key={i}>
|
|
||||||
{renderContentPart(tool, submitApproval, cancelApproval, false)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ToolGroupCard>
|
|
||||||
{afterText.map((p, i) => <p key={`after-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -178,7 +195,7 @@ const MessageBubble = ({
|
|||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
readonly role: "user" | "assistant";
|
readonly role: "user" | "assistant";
|
||||||
readonly children: React.ReactNode;
|
readonly children: ReactNode;
|
||||||
}) => (
|
}) => (
|
||||||
<div className="assistant-message" data-role={role}>
|
<div className="assistant-message" data-role={role}>
|
||||||
{role === "user" ? (
|
{role === "user" ? (
|
||||||
@@ -209,17 +226,10 @@ export const AssistantOperatorThread = ({
|
|||||||
<div className="assistant-thread">
|
<div className="assistant-thread">
|
||||||
<div className="assistant-thread__viewport">
|
<div className="assistant-thread__viewport">
|
||||||
{projected.map((message) => {
|
{projected.map((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 }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (message.role === "user") {
|
if (message.role === "user") {
|
||||||
return (
|
return (
|
||||||
<MessageBubble key={message.id} role="user">
|
<MessageBubble key={message.id} role="user">
|
||||||
{parts.filter((p) => p.type === "text").map((p, i) => (
|
{message.content.filter((p) => p.type === "text").map((p, i) => (
|
||||||
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
|
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
|
||||||
))}
|
))}
|
||||||
</MessageBubble>
|
</MessageBubble>
|
||||||
@@ -229,7 +239,7 @@ export const AssistantOperatorThread = ({
|
|||||||
return (
|
return (
|
||||||
<MessageBubble key={message.id} role="assistant">
|
<MessageBubble key={message.id} role="assistant">
|
||||||
<AssistantMessageBody
|
<AssistantMessageBody
|
||||||
parts={parts}
|
parts={message.content}
|
||||||
submitApproval={submitApproval}
|
submitApproval={submitApproval}
|
||||||
cancelApproval={cancelApproval}
|
cancelApproval={cancelApproval}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("assistantRuntimeProjection", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects tool results as text summary", () => {
|
it("projects tool results as structured result parts", () => {
|
||||||
const messages: ReadonlyArray<AgentMessage> = [
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
{
|
{
|
||||||
id: "tool-result-message",
|
id: "tool-result-message",
|
||||||
@@ -57,13 +57,15 @@ describe("assistantRuntimeProjection", () => {
|
|||||||
|
|
||||||
const projected = projectAgentMessagesForAssistant(messages);
|
const projected = projectAgentMessagesForAssistant(messages);
|
||||||
|
|
||||||
expect(projected[0]).toMatchObject({
|
expect(projected[0]?.content).toEqual([
|
||||||
id: "tool-result-message",
|
{
|
||||||
role: "assistant",
|
type: "tool-result",
|
||||||
content: [
|
toolCallId: "call-1",
|
||||||
{ type: "text", text: "Result for readRunTrace: success" },
|
toolName: "readRunTrace",
|
||||||
],
|
status: "success",
|
||||||
});
|
result: { frames: 3 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects approval requests as human tool calls with contract metadata", () => {
|
it("projects approval requests as human tool calls with contract metadata", () => {
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ export type AssistantToolRenderPayload = {
|
|||||||
readonly isError?: boolean;
|
readonly isError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON-serializable args for assistant-ui compatibility
|
export type AssistantContentPart =
|
||||||
type JsonArgs = Record<string, any>;
|
|
||||||
|
|
||||||
type AssistantContentPart =
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
| {
|
| {
|
||||||
readonly type: "tool-call";
|
readonly type: "tool-call";
|
||||||
readonly toolCallId?: string;
|
readonly toolCallId?: string;
|
||||||
readonly toolName: string;
|
readonly toolName: string;
|
||||||
readonly args?: JsonArgs;
|
readonly args?: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "tool-result";
|
||||||
|
readonly toolCallId: string;
|
||||||
|
readonly toolName: string;
|
||||||
|
readonly status: "success" | "failure";
|
||||||
|
readonly result: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssistantProjectedMessage = {
|
export type AssistantProjectedMessage = {
|
||||||
@@ -35,19 +39,22 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
toolCallId: part.call.id,
|
toolCallId: part.call.id,
|
||||||
toolName: part.call.name,
|
toolName: part.call.name,
|
||||||
args: part.call.input as JsonArgs,
|
args: part.call.input,
|
||||||
}];
|
}];
|
||||||
case "tool-result":
|
case "tool-result":
|
||||||
return [{
|
return [{
|
||||||
type: "text",
|
type: "tool-result",
|
||||||
text: `Result for ${part.result.name}: ${part.result.status === "failure" ? "error" : "success"}`,
|
toolCallId: part.result.callId,
|
||||||
|
toolName: part.result.name,
|
||||||
|
status: part.result.status,
|
||||||
|
result: part.result.output,
|
||||||
}];
|
}];
|
||||||
case "presentation-action":
|
case "presentation-action":
|
||||||
return [{
|
return [{
|
||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
toolCallId: `presentation-${part.action.type}`,
|
toolCallId: `presentation-${part.action.type}`,
|
||||||
toolName: `presentation.${part.action.type}`,
|
toolName: `presentation.${part.action.type}`,
|
||||||
args: part.action as JsonArgs,
|
args: part.action,
|
||||||
}];
|
}];
|
||||||
case "approval-request":
|
case "approval-request":
|
||||||
return [
|
return [
|
||||||
@@ -59,7 +66,7 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
|||||||
args: {
|
args: {
|
||||||
prompt: part.prompt,
|
prompt: part.prompt,
|
||||||
contract: part.contract,
|
contract: part.contract,
|
||||||
} as JsonArgs,
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case "error":
|
case "error":
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("ProblemLoopScene", () => {
|
|||||||
const transcript = screen.getByRole("log", { name: /one-off assistant transcript/i });
|
const transcript = screen.getByRole("log", { name: /one-off assistant transcript/i });
|
||||||
expect(transcript).toHaveClass("assistant-operator-thread");
|
expect(transcript).toHaveClass("assistant-operator-thread");
|
||||||
expect(within(transcript).getByText("Can you finish this workspace task?")).toBeInTheDocument();
|
expect(within(transcript).getByText("Can you finish this workspace task?")).toBeInTheDocument();
|
||||||
const toolButtons = within(transcript).getAllByRole("button", { name: /workflow.run_once/i });
|
const toolButtons = within(transcript).getAllByRole("button", { name: /fetchData|transformPayload|writeOutput/i });
|
||||||
expect(toolButtons).toHaveLength(3);
|
expect(toolButtons).toHaveLength(3);
|
||||||
expect(within(transcript).getByText("Done. But none of this is recorded in a durable workflow.")).toBeInTheDocument();
|
expect(within(transcript).getByText("Done. But none of this is recorded in a durable workflow.")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
import type { AgentToolName } from "../../demo/agent/tools.js";
|
|
||||||
import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
|
import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
|
||||||
import { StageCaption } from "../StageCaption.js";
|
import { StageCaption } from "../StageCaption.js";
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||||
@@ -20,7 +19,7 @@ const oneOffToolLoopMessages: ReadonlyArray<AgentMessage> = [
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
call: {
|
call: {
|
||||||
id: "scene-2-tool-1",
|
id: "scene-2-tool-1",
|
||||||
name: "workflow.run_once" as AgentToolName,
|
name: "fetchData",
|
||||||
input: { source: "api", endpoint: "/tasks" },
|
input: { source: "api", endpoint: "/tasks" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -28,7 +27,7 @@ const oneOffToolLoopMessages: ReadonlyArray<AgentMessage> = [
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
call: {
|
call: {
|
||||||
id: "scene-2-tool-2",
|
id: "scene-2-tool-2",
|
||||||
name: "workflow.run_once" as AgentToolName,
|
name: "transformPayload",
|
||||||
input: { format: "json", validate: true },
|
input: { format: "json", validate: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -36,7 +35,7 @@ const oneOffToolLoopMessages: ReadonlyArray<AgentMessage> = [
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
call: {
|
call: {
|
||||||
id: "scene-2-tool-3",
|
id: "scene-2-tool-3",
|
||||||
name: "workflow.run_once" as AgentToolName,
|
name: "writeOutput",
|
||||||
input: { destination: "file", path: "/tmp/result.json" },
|
input: { destination: "file", path: "/tmp/result.json" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -70,7 +69,7 @@ export const ProblemLoopScene = ({ scene, beat }: ProblemLoopSceneProps) => {
|
|||||||
<h2>Chat + tool loop</h2>
|
<h2>Chat + tool loop</h2>
|
||||||
<p>Good at getting through one request.</p>
|
<p>Good at getting through one request.</p>
|
||||||
</header>
|
</header>
|
||||||
<div aria-label="one-off assistant transcript" role="group">
|
<div className="problem-chat-card__transcript" aria-label="one-off assistant transcript" role="group">
|
||||||
<AssistantOperatorThread mode="dock" messages={oneOffToolLoopMessages} ariaLabel="one-off assistant transcript" />
|
<AssistantOperatorThread mode="dock" messages={oneOffToolLoopMessages} ariaLabel="one-off assistant transcript" />
|
||||||
</div>
|
</div>
|
||||||
<p className="problem-artifact-note">The useful work lives in the conversation history.</p>
|
<p className="problem-artifact-note">The useful work lives in the conversation history.</p>
|
||||||
|
|||||||
@@ -1406,9 +1406,10 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(24rem, 0.95fr) auto minmax(26rem, 1.05fr);
|
grid-template-columns: minmax(24rem, 0.92fr) auto minmax(26rem, 1.08fr);
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-chat-card,
|
.problem-chat-card,
|
||||||
@@ -1416,13 +1417,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid color-mix(in oklch, var(--stage-line) 62%, transparent);
|
border: 1px solid color-mix(in oklch, var(--stage-line) 62%, transparent);
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.95rem;
|
||||||
padding: 0.85rem;
|
padding: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-chat-card {
|
.problem-chat-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
gap: 0.65rem;
|
gap: 0.45rem;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, color-mix(in oklch, var(--stage-inset) 94%, black), var(--stage-inset));
|
linear-gradient(180deg, color-mix(in oklch, var(--stage-inset) 94%, black), var(--stage-inset));
|
||||||
}
|
}
|
||||||
@@ -1456,14 +1458,23 @@
|
|||||||
.problem-artifact-header h2 {
|
.problem-artifact-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font: 800 1.45rem/0.98 var(--font-interface);
|
font: 800 1.32rem/0.98 var(--font-interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-artifact-header p,
|
.problem-artifact-header p,
|
||||||
.problem-artifact-note {
|
.problem-artifact-note {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font: 0.86rem/1.3 var(--font-interface);
|
font: 0.8rem/1.25 var(--font-interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-chat-card .problem-artifact-note {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-chat-card__transcript {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-blueprint .concept-rail {
|
.problem-blueprint .concept-rail {
|
||||||
@@ -1476,7 +1487,7 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.72rem 0.45rem;
|
padding: 0.55rem 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-blueprint .concept-node__copy span,
|
.problem-blueprint .concept-node__copy span,
|
||||||
@@ -1599,9 +1610,15 @@
|
|||||||
|
|
||||||
.assistant-operator-thread {
|
.assistant-operator-thread {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-operator-thread[data-mode="hidden"] {
|
.assistant-operator-thread[data-mode="hidden"] {
|
||||||
@@ -1624,9 +1641,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-thread {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--stage-line) 70%, transparent);
|
||||||
|
border-radius: 0.72rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-inset) 86%, black);
|
||||||
|
}
|
||||||
|
|
||||||
.assistant-thread__viewport {
|
.assistant-thread__viewport {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
padding: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-thread__viewport::-webkit-scrollbar {
|
.assistant-thread__viewport::-webkit-scrollbar {
|
||||||
@@ -1635,8 +1660,8 @@
|
|||||||
|
|
||||||
.assistant-message {
|
.assistant-message {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
gap: 0.36rem;
|
||||||
margin-bottom: 0.65rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message[data-role="user"] {
|
.assistant-message[data-role="user"] {
|
||||||
@@ -1644,96 +1669,101 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message__user-bubble {
|
.assistant-message__user-bubble {
|
||||||
background: color-mix(in oklch, var(--accent-cyan) 14%, var(--stage-surface));
|
background: color-mix(in oklch, var(--accent-cyan) 24%, var(--stage-inset));
|
||||||
border: 1px solid color-mix(in oklch, var(--accent-cyan) 30%, var(--stage-line));
|
border: 1px solid color-mix(in oklch, var(--accent-cyan) 44%, var(--stage-line));
|
||||||
border-radius: 0.65rem;
|
border-radius: 0.55rem;
|
||||||
padding: 0.55rem 0.75rem;
|
padding: 0.48rem 0.65rem;
|
||||||
max-width: 80%;
|
max-width: 76%;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font: 0.82rem/1.5 var(--font-interface);
|
font: 0.76rem/1.35 var(--font-interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message[data-role="assistant"] {
|
.assistant-message[data-role="assistant"] {
|
||||||
justify-items: start;
|
justify-items: start;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-card {
|
/*
|
||||||
border: 1px solid var(--stage-line);
|
assistant-ui ships utility-class components. Bridge their stable data-slot
|
||||||
border-radius: 0.65rem;
|
hooks into the presentation tokens instead of editing generated files.
|
||||||
background: var(--stage-inset);
|
*/
|
||||||
|
.assistant-operator-thread [data-slot="tool-group-root"],
|
||||||
|
.assistant-operator-thread [data-slot="tool-fallback-root"] {
|
||||||
|
width: min(100%, 30rem);
|
||||||
|
border: 1px solid color-mix(in oklch, var(--stage-line) 72%, transparent);
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-surface) 58%, var(--stage-inset));
|
||||||
|
color: var(--text-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-card__trigger {
|
.assistant-operator-thread [data-slot="tool-group-root"] {
|
||||||
display: flex;
|
padding-block: 0;
|
||||||
align-items: center;
|
}
|
||||||
gap: 0.4rem;
|
|
||||||
|
.assistant-operator-thread [data-slot="tool-group-trigger"],
|
||||||
|
.assistant-operator-thread [data-slot="tool-fallback-trigger"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.45rem 0.65rem;
|
min-height: 0;
|
||||||
background: none;
|
gap: 0.32rem;
|
||||||
border: none;
|
padding: 0.3rem 0.48rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font: 700 0.61rem/1 var(--font-evidence);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-operator-thread [data-slot="tool-group-trigger"]:hover,
|
||||||
|
.assistant-operator-thread [data-slot="tool-fallback-trigger"]:hover {
|
||||||
|
background: color-mix(in oklch, var(--accent-cyan) 9%, transparent);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font: 600 0.78rem/1 var(--font-interface);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-card__trigger:hover {
|
.assistant-operator-thread [data-slot="tool-fallback-trigger-icon"],
|
||||||
background: color-mix(in oklch, var(--accent-cyan) 8%, transparent);
|
.assistant-operator-thread [data-slot="tool-fallback-trigger-chevron"],
|
||||||
|
.assistant-operator-thread [data-slot="tool-group-trigger-chevron"] {
|
||||||
|
width: 0.68rem;
|
||||||
|
height: 0.68rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-card__body {
|
.assistant-operator-thread [data-slot="tool-group-content"] > div,
|
||||||
padding: 0 0.65rem 0.55rem;
|
.assistant-operator-thread [data-slot="tool-fallback-content"] > div {
|
||||||
|
gap: 0.22rem;
|
||||||
|
padding: 0.3rem 0.48rem 0.42rem;
|
||||||
|
border-top: 1px solid color-mix(in oklch, var(--stage-line) 55%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-card__io {
|
.assistant-operator-thread [data-slot="tool-group-content"] [data-slot="tool-fallback-root"] {
|
||||||
background: var(--stage-surface);
|
width: fit-content;
|
||||||
border: 1px solid var(--stage-line);
|
max-width: 100%;
|
||||||
border-radius: 0.5rem;
|
background: color-mix(in oklch, var(--stage-inset) 86%, black);
|
||||||
|
border-color: color-mix(in oklch, var(--stage-line) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-operator-thread [data-slot="tool-fallback-args-value"],
|
||||||
|
.assistant-operator-thread [data-slot="tool-fallback-result-content"] {
|
||||||
|
max-height: 6.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--stage-line) 55%, transparent);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: color-mix(in oklch, var(--stage-inset) 72%, black);
|
||||||
|
color: var(--text-secondary);
|
||||||
padding: 0.45rem 0.55rem;
|
padding: 0.45rem 0.55rem;
|
||||||
font: 0.72rem/1.45 var(--font-mono);
|
font: 0.68rem/1.38 var(--font-mono);
|
||||||
color: var(--text-secondary);
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
overflow-wrap: anywhere;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-group {
|
.assistant-operator-thread [data-slot="tool-fallback-result-header"],
|
||||||
border: 1px solid var(--stage-line);
|
.assistant-operator-thread [data-slot="tool-fallback-error-header"] {
|
||||||
border-radius: 0.65rem;
|
margin: 0;
|
||||||
background: var(--stage-inset);
|
color: var(--text-muted);
|
||||||
overflow: hidden;
|
font: 700 0.66rem/1 var(--font-evidence);
|
||||||
}
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
.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;
|
|
||||||
max-height: 14rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-tool-group__body .assistant-tool-card {
|
|
||||||
border-color: var(--stage-line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-tool-approval {
|
.assistant-tool-approval {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "tw-shimmer";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-paper: #faf8f5;
|
--color-paper: #faf8f5;
|
||||||
--color-ink: #1a1a1a;
|
--color-ink: #1a1a1a;
|
||||||
|
|||||||
Reference in New Issue
Block a user