Rename chat artifacts copy to canvas (#6298)

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
This commit is contained in:
Wasim Yousef Said 2026-06-15 13:38:39 +02:00 committed by GitHub
commit 9d7740a82f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 66 additions and 66 deletions

View file

@ -773,10 +773,10 @@ RENDER_HTML_TOOL = {
"function": {
"name": "render_html",
"description": (
"Render a self-contained HTML/CSS/JavaScript artifact for the user. "
"Render a self-contained HTML/CSS/JavaScript canvas for the user. "
"Call this at most once per assistant response unless the user "
"explicitly asks for changes in that response. Future user requests "
"for new artifacts may call render_html once. Put the entire document "
"for new canvases may call render_html once. Put the entire document "
"in code, including any CSS in <style> tags and JavaScript in <script> tags."
),
"parameters": {
@ -788,7 +788,7 @@ RENDER_HTML_TOOL = {
},
"title": {
"type": "string",
"description": "Short display title for the artifact.",
"description": "Short display title for the canvas.",
},
},
"required": ["code"],
@ -954,14 +954,14 @@ def _render_html_result(arguments: dict) -> str:
if isinstance(title, str) and title.strip():
safe_title = title.strip()[:120]
return (
f"Rendered HTML artifact: {safe_title}. Do not call render_html "
f"Rendered HTML canvas: {safe_title}. Do not call render_html "
"again in this response unless the user asks for changes. For a later "
"user request for a new artifact, call render_html once."
"user request for a new canvas, call render_html once."
)
return (
"Rendered HTML artifact. Do not call render_html again in this response "
"Rendered HTML canvas. Do not call render_html again in this response "
"unless the user asks for changes. For a later user request for a new "
"artifact, call render_html once."
"canvas, call render_html once."
)

View file

@ -954,7 +954,7 @@ _ARTIFACT_PREVIEW_FRAME_HTML = """<!doctype html>
configurable: true,
});
} catch {
// Leave the sandbox failure contained in the artifact if the
// Leave the sandbox failure contained in the canvas if the
// browser refuses to shadow the Web Storage accessor.
}
};
@ -986,7 +986,7 @@ async def artifact_preview_frame(
allow_network: bool = False,
token: Optional[str] = None,
):
"""Serve the opaque sandbox shell used for client-side HTML artifacts."""
"""Serve the opaque sandbox shell used for client-side HTML canvases."""
if allow_network:
auth_header = request.headers.get("authorization")
@ -1212,12 +1212,12 @@ async def _await_disconnect_then_close(request, resp, cancel_event) -> None:
# Centralized local/server tool nudge. Keep render_html guidance gated to turns
# where the artifact tool is actually present in the tool schema; otherwise
# where the canvas tool is actually present in the tool schema; otherwise
# small local models can hallucinate a missing tool call instead of following
# the fenced-HTML fallback prompt.
_TOOL_BASE_NUDGE = (
"Tools are available when they materially improve the answer. Use an enabled "
"tool for current facts, calculations, code execution, or artifacts when it "
"tool for current facts, calculations, code execution, or canvases when it "
"materially helps; otherwise answer normally and follow the user's requested "
"format."
)
@ -1233,11 +1233,11 @@ _TOOL_CODE_TIP = (
"and analyze information from tool results."
)
_TOOL_ARTIFACT_TIP = (
"For HTML, CSS, or JavaScript artifact requests, call render_html once when "
"For HTML, CSS, or JavaScript canvas requests, call render_html once when "
"it is available with one complete self-contained HTML document in the code "
"argument. After render_html succeeds, do not call it again in the same "
"response unless the user asks for changes. Future user requests for new "
"artifacts may call render_html once."
"canvases may call render_html once."
)

View file

@ -72,7 +72,7 @@ class TestToolActionNudge:
assert "Use code execution for math" in nudge
assert "render_html" not in nudge
def test_balanced_nudge_preserves_compact_web_tip_and_artifact_gate(self):
def test_balanced_nudge_preserves_compact_web_tip_and_canvas_gate(self):
nudge = _build_tool_action_nudge(
tools = [
{"type": "function", "function": {"name": "web_search"}},
@ -840,7 +840,7 @@ class TestAnthropicToolNonStreaming:
"type": "tool_end",
"tool_name": "render_html",
"tool_call_id": "call_0",
"result": "Rendered HTML artifact.",
"result": "Rendered HTML canvas.",
}
response = asyncio.run(_anthropic_tool_non_streaming(_run_gen, "msg_1", "m"))

View file

@ -106,7 +106,7 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
tool_call_id = "call_render_late"
first_stream = [
_sse({"content": "Here is the artifact.\n\n"}),
_sse({"content": "Here is the canvas.\n\n"}),
_sse(
{
"tool_calls": [
@ -140,7 +140,7 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
def fake_execute_tool(name, arguments, **_kwargs):
calls.append((name, arguments))
return "Rendered HTML artifact: Simple Red Square."
return "Rendered HTML canvas: Simple Red Square."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
@ -168,7 +168,7 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
)
content_events = [e for e in events if e.get("type") == "content"]
assert content_events[0]["text"] == "Here is the artifact.\n\n"
assert content_events[0]["text"] == "Here is the canvas.\n\n"
first_content_index = next(
i for i, event in enumerate(events) if event.get("type") == "content"
@ -195,7 +195,7 @@ def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch):
# plus the structured tool call, preserving OpenAI-compatible ordering.
assert len(payloads) == 2
assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"]
assert assistant_messages[-1]["content"] == "Here is the artifact.\n\n"
assert assistant_messages[-1]["content"] == "Here is the canvas.\n\n"
assert assistant_messages[-1]["tool_calls"][0]["id"] == tool_call_id
assert assistant_messages[-1]["tool_calls"][0]["function"]["name"] == "render_html"
@ -257,7 +257,7 @@ def test_repeat_render_html_nudge_is_not_user_visible_error(monkeypatch):
def fake_execute_tool(name, arguments, **_kwargs):
calls.append((name, arguments))
return "Rendered HTML artifact: First."
return "Rendered HTML canvas: First."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
@ -346,7 +346,7 @@ def test_render_html_success_drops_tool_schema_before_final_pass(monkeypatch):
backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads)
def fake_execute_tool(name, arguments, **_kwargs):
return "Rendered HTML artifact: Done."
return "Rendered HTML canvas: Done."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
@ -783,7 +783,7 @@ def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(m
def fake_execute_tool(name, arguments, **_kwargs):
calls.append((name, arguments))
return "Rendered HTML artifact: One."
return "Rendered HTML canvas: One."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
@ -869,7 +869,7 @@ def test_render_html_success_does_not_reprompt_render_html_intent(monkeypatch):
The post-tool model pass can say it will use render_html again without
emitting a tool call. That should be accepted as a final model mistake,
not turned into repeated internal re-prompts after the artifact already
not turned into repeated internal re-prompts after the canvas already
exists.
"""
@ -907,7 +907,7 @@ def test_render_html_success_does_not_reprompt_render_html_intent(monkeypatch):
def fake_execute_tool(name, arguments, **_kwargs):
calls.append((name, arguments))
return "Rendered HTML artifact: First."
return "Rendered HTML canvas: First."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)
@ -1146,7 +1146,7 @@ def test_reprompted_tool_call_still_streams_final_answer(monkeypatch):
def fake_execute_tool(name, arguments, **_kwargs):
calls.append((name, arguments))
return "Rendered HTML artifact: Forced."
return "Rendered HTML canvas: Forced."
monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool)

View file

@ -264,7 +264,7 @@ def _make_loop(
def test_active_tools_are_passed_to_single_turn_after_render_html_success():
captured_tool_names: list[list[str]] = []
exec_fn = FakeExecuteTool(["Rendered HTML artifact."])
exec_fn = FakeExecuteTool(["Rendered HTML canvas."])
def fake_single_turn(_messages, *, active_tools = None):
captured_tool_names.append(
@ -357,7 +357,7 @@ class TestLoopBasic:
assert "Result: 1" in contents[-1]["text"]
def test_render_html_emits_provisional_tool_start(self):
exec_fn = FakeExecuteTool(["Rendered HTML artifact."])
exec_fn = FakeExecuteTool(["Rendered HTML canvas."])
turn_iter = iter(
[
[
@ -412,8 +412,8 @@ class TestLoopBasic:
assert tool_starts[0]["tool_name"] == "python"
assert exec_fn.calls == [("python", {"code": "print('<function=render_html>')"})]
def test_render_html_success_blocks_second_artifact_call(self):
exec_fn = FakeExecuteTool(["Rendered HTML artifact."])
def test_render_html_success_blocks_second_canvas_call(self):
exec_fn = FakeExecuteTool(["Rendered HTML canvas."])
turn_iter = iter(
[
[

View file

@ -191,7 +191,7 @@ def test_render_html_success_filters_active_tools_and_repeat_is_internal():
]
first = controller.prepare_call(_call("render_html", {"code": "<html></html>"}, "call_html_1"))
controller.record_result(first, "Rendered HTML artifact: Demo")
controller.record_result(first, "Rendered HTML canvas: Demo")
assert [t["function"]["name"] for t in controller.active_tools()] == ["web_search"]

View file

@ -76,7 +76,7 @@ function isRenderableRenderHtmlToolPart(part: unknown): boolean {
}
if (
typeof toolPart.result === "string" &&
toolPart.result.startsWith("Rendered HTML artifact")
toolPart.result.startsWith("Rendered HTML canvas")
) {
return true;
}
@ -286,7 +286,7 @@ function CodeBlockActions({
}
// DiffusionGemma renders its denoising live in the bubble (see DiffusionCanvas in
// thread.tsx) and has the artifacts canvas on by default, so a full-HTML answer
// thread.tsx) and has the HTML canvas feature on by default, so a full-HTML answer
// (e.g. a playable game) renders as an interactive card without the global toggle.
function StreamdownBlock(props: BlockProps) {
const shouldCollapseHtmlArtifacts = useChatRuntimeStore(
@ -335,7 +335,7 @@ function StreamdownBlock(props: BlockProps) {
) {
return (
<div className="my-4 flex h-48 items-center justify-center rounded-xl border border-border bg-muted/30 text-sm text-muted-foreground animate-pulse">
Loading artifact preview...
Loading canvas preview...
</div>
);
}

View file

@ -243,7 +243,7 @@ const ToolGroupImpl: FC<
const forceOpen =
hasPendingConfirmation || (forcedOpenRef.current && messageRunning);
// Render single tool calls and artifacts directly so cards never hide in a
// Render single tool calls and canvases directly so cards never hide in a
// collapsed group.
if (toolCount <= 1 || containsArtifactTool) {
return <>{children}</>;

View file

@ -37,12 +37,12 @@ const RenderHtmlToolUIImpl: ToolCallMessagePartComponent = ({
const code = typeof parsedArgs.code === "string" ? parsedArgs.code : "";
const hasCode = code.trim().length > 0;
const title =
typeof parsedArgs.title === "string" ? parsedArgs.title : "HTML artifact";
typeof parsedArgs.title === "string" ? parsedArgs.title : "HTML canvas";
const isRunning = status?.type === "running";
const codeIsStreaming = propStatus.code === "streaming";
// Surface the backend error when the tool call completed with invalid args.
// Success results start with "Rendered HTML artifact"; errors with "Error:".
// Success results start with "Rendered HTML canvas"; errors with "Error:".
const errorText =
status?.type === "complete" &&
typeof result === "string" &&
@ -118,16 +118,16 @@ const RenderHtmlToolUIImpl: ToolCallMessagePartComponent = ({
<span className="grid min-w-0 flex-1 gap-1">
<span className="truncate text-sm font-medium leading-tight text-foreground">
{errorText
? "Artifact error"
? "Canvas error"
: isStaleGeneratingArtifact
? "Artifact interrupted"
: "Artifact unavailable"}
? "Canvas interrupted"
: "Canvas unavailable"}
</span>
<span className="truncate text-[11px] leading-none text-muted-foreground">
{errorText ??
(isStaleGeneratingArtifact
? "Refresh stopped this preview"
: "HTML artifact")}
: "HTML canvas")}
</span>
</span>
</div>

View file

@ -1841,8 +1841,8 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
const hasOutboundImage = Boolean(imageBase64);
// Keep render_html local-only and mirror the backend image-turn gate.
// Artifacts are independent of Search/Code: a local tool-capable model
// with Artifacts on exposes render_html even with no other pills active.
// Canvas is independent of Search/Code: a local tool-capable model
// with Canvas on exposes render_html even with no other pills active.
const renderHtmlToolEnabledForThisTurn = Boolean(
!isExternalRequest &&
supportsTools &&
@ -1851,12 +1851,12 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
);
const artifactInstruction = artifactsEnabled
? renderHtmlToolEnabledForThisTurn
? "When the user asks for an HTML, CSS, or JavaScript artifact, call render_html once with one complete self-contained HTML document in the code argument. Embed CSS and JavaScript inside the document. After render_html succeeds, do not call it again in the same response unless the user asks for changes. Future user requests for new artifacts may call render_html once."
: "When the user asks for an HTML, CSS, or JavaScript artifact, return one complete self-contained fenced html code block. Embed CSS and JavaScript inside the document. Do not emit tool-call syntax."
? "When the user asks for an HTML, CSS, or JavaScript canvas, call render_html once with one complete self-contained HTML document in the code argument. Embed CSS and JavaScript inside the document. After render_html succeeds, do not call it again in the same response unless the user asks for changes. Future user requests for new canvases may call render_html once."
: "When the user asks for an HTML, CSS, or JavaScript canvas, return one complete self-contained fenced html code block. Embed CSS and JavaScript inside the document. Do not emit tool-call syntax."
: null;
const effectiveDisabledToolGuard =
disabledToolGuard && artifactsEnabled
? `${disabledToolGuard} HTML, CSS, or JavaScript artifact requests can still be answered by following the artifact fallback instruction.`
? `${disabledToolGuard} HTML, CSS, or JavaScript canvas requests can still be answered by following the canvas fallback instruction.`
: disabledToolGuard;
addSystemInstruction(outboundMessages, effectiveDisabledToolGuard);
addSystemInstruction(outboundMessages, artifactInstruction);
@ -3091,7 +3091,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
closeReasoningContent();
cumulativeText += delta;
}
// Strip a trailing ${...} template-literal artifact from
// Strip a trailing ${...} template-literal fragment from
// external streams (mistral magistral occasionally emits one).
if (isExternalRequest) {
cumulativeText = cumulativeText.replace(

View file

@ -127,7 +127,7 @@ export function ArtifactCard({
{artifact.title}
</span>
<span className="truncate text-[11px] leading-none text-muted-foreground">
HTML artifact
HTML canvas
</span>
</span>
{isStreaming ? (

View file

@ -47,9 +47,9 @@ function buildHtmlFence(source: string): string {
const fence = "`".repeat(longestBacktickRun + 1);
return `${fence}html\n${source}\n${fence}`;
}
// Sandboxed artifact iframes are deliberately outside the overlay focus trap:
// Sandboxed canvas iframes are deliberately outside the overlay focus trap:
// granting same-origin sandbox privileges would weaken isolation, so reaching
// interactive artifact content via keyboard is a known sandbox limitation.
// interactive canvas content via keyboard is a known sandbox limitation.
const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
@ -85,7 +85,7 @@ function ArtifactGeneratingPanel() {
className="mx-auto mb-3 size-20 object-contain"
/>
<p className="text-sm font-medium text-foreground">
Building artifact preview
Building canvas preview
</p>
<p className="text-xs leading-relaxed text-muted-foreground">
The preview will appear here when the HTML is ready.
@ -206,7 +206,7 @@ export function ArtifactSurface({
? "artifact-panel-shell mx-2 mt-[72px] mb-8 h-[calc(100%_-_104px)] overflow-visible rounded-[28px] border-t border-border/70 bg-card/95"
: "h-[min(92vh,900px)] w-[min(96vw,1200px)] overflow-hidden rounded-2xl border border-border shadow-xl",
)}
aria-label={`${artifact.title} artifact`}
aria-label={`${artifact.title} canvas`}
>
<header
className={cn(
@ -217,7 +217,7 @@ export function ArtifactSurface({
<div
className="flex items-center gap-1 rounded-full bg-muted/40 p-0.5"
role="tablist"
aria-label="Artifact view"
aria-label="Canvas view"
>
{(["preview", "source"] as const).map((mode) => {
const isPreview = mode === "preview";
@ -239,7 +239,7 @@ export function ArtifactSurface({
"cursor-not-allowed opacity-50",
)}
aria-label={
isPreview ? "Preview artifact" : "View artifact source"
isPreview ? "Preview canvas" : "View canvas source"
}
aria-selected={effectiveViewMode === mode}
aria-pressed={effectiveViewMode === mode}
@ -264,7 +264,7 @@ export function ArtifactSurface({
className="size-8"
disabled={isLoadingArtifact || !hasArtifactCode}
onClick={() => downloadTextFile(filename, artifact.code)}
aria-label="Download artifact HTML"
aria-label="Download canvas HTML"
>
<HugeiconsIcon icon={Download01Icon} className="size-4" />
</Button>
@ -275,7 +275,7 @@ export function ArtifactSurface({
className="size-8"
disabled={isLoadingArtifact || !hasArtifactCode}
onClick={handleCopy}
aria-label="Copy artifact HTML"
aria-label="Copy canvas HTML"
>
{copied ? (
<CheckIcon className="size-4" />
@ -290,7 +290,7 @@ export function ArtifactSurface({
size="icon"
className="size-8"
onClick={onOpenFullscreen}
aria-label="Open artifact fullscreen"
aria-label="Open canvas fullscreen"
>
<Maximize2Icon className="size-4" />
</Button>
@ -301,7 +301,7 @@ export function ArtifactSurface({
size="icon"
className="size-8"
onClick={onClose}
aria-label="Close artifact"
aria-label="Close canvas"
>
<XIcon className="size-4" />
</Button>

View file

@ -28,12 +28,12 @@ export function buildArtifactSrcDoc(code: string): string {
return `${code}\n${resizeScript}`;
}
// Preview iframes intentionally omit allow-downloads: generated artifacts can
// Preview iframes intentionally omit allow-downloads: generated canvases can
// offer their own UI, but downloads must go through Studio's explicit
// copy/download controls outside the no-same-origin sandbox.
export function ArtifactHtmlFrame({
code,
title = "HTML artifact preview",
title = "HTML canvas preview",
className,
fill = false,
}: {

View file

@ -26,7 +26,7 @@ export interface ChatArtifactInput {
isStreaming?: boolean;
}
const DEFAULT_ARTIFACT_TITLE = "HTML artifact";
const DEFAULT_ARTIFACT_TITLE = "HTML canvas";
export function normalizeArtifactTitle(title?: string | null): string {
const trimmed = title?.trim();
@ -80,5 +80,5 @@ export function getArtifactFilename(
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48);
return `${slug || "artifact"}.html`;
return `${slug || "canvas"}.html`;
}

View file

@ -1493,7 +1493,7 @@ export function ChatPage(): ReactElement {
if (view.mode !== "single") return;
if (view.threadId || view.newThreadNonce || !selectedArtifact) return;
// view excludes __LOCALID_ threads (they fall through to mode:"single"
// with no threadId/nonce). Don't close an artifact whose thread is the
// with no threadId/nonce). Don't close a canvas whose thread is the
// active local thread.
if (
selectedArtifact.threadId &&

View file

@ -197,13 +197,13 @@ export const en = {
title: "Chat",
description: "Manage chat history stored on this device.",
artifacts: {
title: "Artifacts",
title: "Canvas",
collapseHtmlBlocks: "Collapse HTML blocks",
collapseHtmlBlocksDescription:
"Artifacts mode collapses full HTML automatically. Turn on to also collapse fenced HTML documents when Artifacts is off.",
allowNetworkAccess: "Allow artifact network access",
"Canvas mode collapses full HTML automatically. Turn this on to also collapse fenced HTML documents when Canvas is off.",
allowNetworkAccess: "Allow canvas network access",
allowNetworkAccessDescription:
"Let artifact previews load scripts, styles, fonts, media, and network resources from CDNs. Keep off for fully offline previews.",
"Let canvas previews load scripts, styles, fonts, media, and network resources from CDNs. Keep off for fully offline previews.",
},
data: "Data",
exportHistory: "Export chat history",