diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index 67eb146a7e..f1ef5bd425 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -871,7 +871,8 @@ export type ChannelActionType = | "copy_link" | "mention_member" | "view_activity" - | "open_mention"; + | "open_mention" + | "canvas_mode_toggle"; export interface ChannelActionProperties { action_type: ChannelActionType; @@ -888,6 +889,8 @@ export interface ChannelActionProperties { mentioned_user_id?: string; /** For new_task_suggestion: the starter-prompt card label. */ suggestion_label?: string; + /** For canvas_mode_toggle: whether canvas mode is being armed. */ + armed?: boolean; /** Whether the underlying mutation resolved successfully. */ success?: boolean; } diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index 8bddc42553..e732ef2b83 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -1,5 +1,8 @@ import { isValidConfigValue } from "@posthog/core/task-detail/configOptions"; +import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { Task } from "@posthog/shared/domain-types"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; import { forwardRef, useCallback, @@ -8,10 +11,13 @@ import { useState, } from "react"; import { useConnectivity } from "../../../hooks/useConnectivity"; +import { track } from "../../../shell/analytics"; +import { useOptionalAuthenticatedClient } from "../../auth/authClient"; import { useUserRepositoryIntegration } from "../../integrations/useIntegrations"; import { PromptInput } from "../../message-editor/components/PromptInput"; import { useDraftStore } from "../../message-editor/draftStore"; import type { EditorHandle } from "../../message-editor/types"; +import { toastError } from "../../notifications/errorDetails"; import { ReasoningLevelSelector } from "../../sessions/components/ReasoningLevelSelector"; import { UnifiedModelSelector } from "../../sessions/components/UnifiedModelSelector"; import { getCurrentModeFromConfigOptions } from "../../sessions/sessionStore"; @@ -27,6 +33,17 @@ import { useCloudModeEnabled } from "../../task-detail/hooks/useCloudModeEnabled import { usePreviewConfig } from "../../task-detail/hooks/usePreviewConfig"; import { useTaskCreation } from "../../task-detail/hooks/useTaskCreation"; import { resolveWorkspaceModePreference } from "../../task-detail/hooks/workspaceModePreference"; +import { trackAndCreateCanvas } from "../createCanvasAnalytics"; +import { channelFeedQueryKey } from "../hooks/useChannelFeed"; +import { + UNTITLED_CANVAS_NAME, + useDashboardMutations, +} from "../hooks/useDashboards"; +import { useGenerateFreeformCanvas } from "../hooks/useGenerateFreeformCanvas"; +import { + normalizeChannelName, + PERSONAL_CHANNEL_NAME, +} from "../hooks/useTaskChannels"; export interface ChannelHomeComposerHandle { /** Drop a starter prompt into the editor and apply its mode, if any. */ @@ -60,6 +77,33 @@ export const ChannelHomeComposer = forwardRef< const editorRef = useRef(null); const [editorIsEmpty, setEditorIsEmpty] = useState(true); const { isOnline } = useConnectivity(); + const navigate = useNavigate(); + + // Canvas mode, armed from the mode selector (like Autoresearch on the + // new-task composer): the next submit generates a canvas from the prompt — + // create a canvas in the channel, kick off freeform generation, and open it — + // instead of creating a plain task. This replaces the prompt-to-canvas entry + // the old channel landing had. + const [canvasArmed, setCanvasArmed] = useState(false); + const { createDashboard } = useDashboardMutations(); + const { generate: generateCanvas, isStarting: isStartingCanvas } = + useGenerateFreeformCanvas({ + channelId, + channelName: channelName ?? "", + // The parent already fetches the channel CONTEXT.md; passing it keeps + // the hook from running its own duplicate fetch. + channelContext, + }); + + const toggleCanvasMode = useCallback(() => { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "canvas_mode_toggle", + surface: "channel_home", + channel_id: channelId, + armed: !canvasArmed, + }); + setCanvasArmed(!canvasArmed); + }, [channelId, canvasArmed]); const { lastUsedAdapter, @@ -123,6 +167,81 @@ export const ChannelHomeComposer = forwardRef< const currentReasoningLevel = thoughtOption?.type === "select" ? thoughtOption.currentValue : undefined; + const queryClient = useQueryClient(); + const apiClient = useOptionalAuthenticatedClient(); + const handleCanvasSubmit = useCallback(async () => { + const instruction = editorRef.current?.getText().trim(); + if (!instruction || isStartingCanvas) return; + // The folder→backend channel mapping can still be resolving when the user + // submits (fresh channel, cold channels list). Resolve it here rather than + // silently creating a run the feed will never show. The personal channel + // can't be resolved by name; it only arrives via the channels list. + let feedChannelId = backendChannelId; + const normalizedName = channelName ? normalizeChannelName(channelName) : ""; + if ( + !feedChannelId && + apiClient && + normalizedName && + normalizedName !== PERSONAL_CHANNEL_NAME + ) { + feedChannelId = await apiClient + .resolveTaskChannel(normalizedName) + .then((c) => c.id) + .catch(() => undefined); + } + let record: { id: string; name: string }; + try { + record = await trackAndCreateCanvas( + channelId, + "freeform", + "channel_home", + () => createDashboard(channelId, UNTITLED_CANVAS_NAME, "freeform"), + ); + } catch (error) { + toastError("Couldn't create canvas", error); + return; + } + // generate() surfaces its own failure toasts; on success it files the task + // to the channel and tracks completion for the finished-generation toast. + const taskId = await generateCanvas({ + dashboardId: record.id, + name: record.name, + templateId: "freeform", + instruction, + // Owned by the backend channel so the run shows as a card in the feed, + // like a plain composer submit. + backendChannelId: feedChannelId, + adapter: adapter ?? "claude", + model: currentModel, + reasoningLevel: currentReasoningLevel, + useStarter: true, + }); + if (!taskId) return; + // Surface the new card without waiting for the feed's next poll. + void queryClient.invalidateQueries({ + queryKey: channelFeedQueryKey(feedChannelId), + }); + editorRef.current?.clear(); + setCanvasArmed(false); + void navigate({ + to: "/website/$channelId/dashboards/$dashboardId", + params: { channelId, dashboardId: record.id }, + }); + }, [ + channelId, + channelName, + backendChannelId, + apiClient, + adapter, + currentModel, + currentReasoningLevel, + createDashboard, + generateCanvas, + isStartingCanvas, + navigate, + queryClient, + ]); + const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({ editorRef, sessionId, @@ -187,36 +306,49 @@ export const ChannelHomeComposer = forwardRef< ); const hints = ["@ to add files", "/ for skills"].join(", "); + const isBusy = isCreatingTask || isStartingCanvas; + const submitComposer = canvasArmed ? handleCanvasSubmit : handleSubmit; return (
-
- -
+ {/* Canvas generation always runs in the cloud, so the local/cloud pick + doesn't apply while canvas mode is armed. */} + {!canvasArmed && ( +
+ +
+ )} @@ -235,14 +367,14 @@ export const ChannelHomeComposer = forwardRef< thoughtOption={thoughtOption} adapter={adapter} onChange={handleThoughtChange} - disabled={isCreatingTask} + disabled={isBusy} /> ) } onEmptyChange={setEditorIsEmpty} - onSubmitClick={handleSubmit} + onSubmitClick={() => void submitComposer()} onSubmit={() => { - if (canSubmit) handleSubmit(); + if (canvasArmed || canSubmit) void submitComposer(); }} />
diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 504703c5cc..d6bc3e269e 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -39,8 +39,8 @@ import { } from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal"; -import { trackAndCreateCanvas } from "@posthog/ui/features/canvas/components/NewCanvasMenu"; import { RenameChannelModal } from "@posthog/ui/features/canvas/components/RenameChannelModal"; +import { trackAndCreateCanvas } from "@posthog/ui/features/canvas/createCanvasAnalytics"; import { useChannelStars, useChannelStarToggle, diff --git a/packages/ui/src/features/canvas/components/NewCanvasMenu.tsx b/packages/ui/src/features/canvas/components/NewCanvasMenu.tsx index 53c50b5f01..c2cbae37ff 100644 --- a/packages/ui/src/features/canvas/components/NewCanvasMenu.tsx +++ b/packages/ui/src/features/canvas/components/NewCanvasMenu.tsx @@ -8,32 +8,14 @@ import { DialogHeader, DialogTitle, } from "@posthog/quill"; -import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; +import { + type CreateSurface, + trackAndCreateCanvas, +} from "@posthog/ui/features/canvas/createCanvasAnalytics"; import { useCanvasTemplates } from "@posthog/ui/features/canvas/hooks/useCanvasTemplates"; import { useCreateAndOpenDashboard } from "@posthog/ui/features/canvas/hooks/useDashboards"; -import { track } from "@posthog/ui/shell/analytics"; import { useState } from "react"; -// Where a canvas create was triggered from, for analytics. -export type CreateSurface = "dashboards_grid" | "sidebar"; - -// Fire the "create" DASHBOARD_ACTION, then create + open the canvas. Exported so -// other entry points (the sidebar "+" dropdown) report creation the same way. -export function trackAndCreateCanvas( - channelId: string | undefined, - templateId: string | undefined, - surface: CreateSurface, - create: () => void, -) { - track(ANALYTICS_EVENTS.DASHBOARD_ACTION, { - action_type: "create", - surface, - channel_id: channelId, - template_id: templateId, - }); - create(); -} - // The list of template options shared by the canvas-create surfaces (the // dashboards-grid dialog and the sidebar "+" dropdown). Picking a template // creates + opens the canvas, then calls `onPicked` (e.g. to close the diff --git a/packages/ui/src/features/canvas/createCanvasAnalytics.ts b/packages/ui/src/features/canvas/createCanvasAnalytics.ts new file mode 100644 index 0000000000..31e5ba9fd1 --- /dev/null +++ b/packages/ui/src/features/canvas/createCanvasAnalytics.ts @@ -0,0 +1,25 @@ +import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; +import { track } from "@posthog/ui/shell/analytics"; + +// Where a canvas create was triggered from, for analytics. +export type CreateSurface = "dashboards_grid" | "sidebar" | "channel_home"; + +// Fire the "create" DASHBOARD_ACTION, then create + open the canvas. Shared so +// every canvas-create entry point (the dashboards-grid dialog, the sidebar "+" +// dropdown, the channel composer's canvas mode) reports creation the same way. +// Returns `create`'s result so callers that need the created record can await +// it. +export function trackAndCreateCanvas( + channelId: string | undefined, + templateId: string | undefined, + surface: CreateSurface, + create: () => T, +): T { + track(ANALYTICS_EVENTS.DASHBOARD_ACTION, { + action_type: "create", + surface, + channel_id: channelId, + template_id: templateId, + }); + return create(); +} diff --git a/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx b/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx index 48ddc60753..c56b124bcb 100644 --- a/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx +++ b/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx @@ -46,11 +46,8 @@ export const FreeformGenerateBar = forwardRef< ref, ) { const { generate, isStarting } = useGenerateFreeformCanvas({ - dashboardId, channelId, - name, channelName, - templateId, }); // On a FIRST build we seed the agent with a known-good starter scaffold by @@ -68,6 +65,9 @@ export const FreeformGenerateBar = forwardRef< const instruction = text.trim(); if (!instruction) return; const taskId = await generate({ + dashboardId, + name, + templateId, instruction, currentCode, useStarter: !isEdit && useStarter, diff --git a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts index 76d84eaedf..3ae5404d7b 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts @@ -10,7 +10,11 @@ import { } from "@posthog/core/task-detail/taskService"; import { useService } from "@posthog/di/react"; import { useHostTRPC } from "@posthog/host-router/react"; -import { getCloudUrlFromRegion, type WorkspaceMode } from "@posthog/shared"; +import { + type Adapter, + getCloudUrlFromRegion, + type WorkspaceMode, +} from "@posthog/shared"; import { useAuthStateValue } from "@posthog/ui/features/auth/store"; import { buildFreeformGenerationPrompt } from "@posthog/ui/features/canvas/freeformPrompt"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; @@ -35,13 +39,17 @@ import { useCallback, useState } from "react"; // server-side regardless of which client kicked it off, and the agent publishes // the result via the `desktop-file-system-canvas-partial-update` MCP tool. export function useGenerateFreeformCanvas(args: { - dashboardId: string; channelId: string; - name: string; channelName: string; - templateId?: string; + /** + * The channel's CONTEXT.md, when the surface already fetched it (the channel + * composer receives it as a prop). Passing the property — even with an + * undefined value — marks the caller as its owner and skips this hook's own + * fetch; omit it entirely to let the hook fetch. + */ + channelContext?: string; }) { - const { dashboardId, channelId, name, channelName, templateId } = args; + const { channelId, channelName } = args; const taskService = useService(TASK_SERVICE); const modelResolver = useService(REPORT_MODEL_RESOLVER); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); @@ -55,14 +63,33 @@ export function useGenerateFreeformCanvas(args: { const { setGenerationTask, renameDashboard } = useDashboardMutations(); // The channel's CONTEXT.md, passed to the agent as optional background so the // generated canvas starts with the shared context. Absent/empty is fine. - const { data: instructions } = useFolderInstructions(channelId); - const channelContext = instructions?.content; + const callerOwnsContext = "channelContext" in args; + const { data: instructions } = useFolderInstructions(channelId, { + enabled: !callerOwnsContext, + }); + const channelContext = callerOwnsContext + ? args.channelContext + : instructions?.content; const [isStarting, setIsStarting] = useState(false); const generate = useCallback( async (opts: { + // The canvas being generated — per call, so surfaces that create the + // canvas at submit time (the channel composer) can use one hook instance. + dashboardId: string; + name: string; + templateId?: string; instruction: string; currentCode?: string; + // Backend channel UUID that owns the created task, so it lands in the + // channel's task feed like a plain composer submit. + backendChannelId?: string; + // The composer's picks, when the surface exposes model/effort selectors. + // The model is validated against the gateway (falling back to the + // adapter's default) so a stale id can't 403 the run. + adapter?: Adapter; + model?: string; + reasoningLevel?: string; // Default on (opt out in the bar): seed the starter scaffold on first build. useStarter?: boolean; // Dev-only override (the bar exposes a local/cloud picker in dev so a @@ -70,25 +97,36 @@ export function useGenerateFreeformCanvas(args: { // always runs in the cloud — see the default below. workspaceMode?: WorkspaceMode; }): Promise => { - setIsStarting(true); - try { + const { + dashboardId, + name, + templateId, + instruction, + currentCode, + backendChannelId, + adapter = "claude", + reasoningLevel, + useStarter, // Defaults to a cloud run — canvas generation should never tie up (or // depend on) the local machine, and it's never the sticky last-used // workspace mode. The dev-only picker can override to "local" to test a // local build of these features before merging. - const workspaceMode = opts.workspaceMode ?? "cloud"; - + workspaceMode = "cloud", + } = opts; + setIsStarting(true); + try { // A cloud run requires an explicit adapter + model (the API rejects a - // cloud runtime without a model). Canvas has no model picker, so resolve - // the adapter's server default the same way the inbox one-click flows do - // — the resolver validates against the gateway, so a stale id can't slip - // through and 403 the run. - let model: string | undefined; + // cloud runtime without a model). Resolve the caller's pick — or the + // adapter's server default when none — the same way the inbox one-click + // flows do; the resolver validates against the gateway, so a stale id + // can't slip through and 403 the run. + let model: string | undefined = opts.model; if (workspaceMode === "cloud") { model = cloudRegion ? await modelResolver.resolveDefaultModel( getCloudUrlFromRegion(cloudRegion), - "claude", + adapter, + opts.model, ) : undefined; if (!model) { @@ -106,19 +144,21 @@ export function useGenerateFreeformCanvas(args: { name, channelName, templateId, - instruction: opts.instruction, - currentCode: opts.currentCode, - useStarter: opts.useStarter, + instruction, + currentCode, + useStarter, }), taskDescription: `Generate canvas "${name}"`, // Unattended generation: run in auto mode so it doesn't stall on edit-approval prompts. executionMode: "auto" as const, workspaceMode, - adapter: "claude", + adapter, model, + reasoningLevel, allowNoRepo: true, channelContext, channelName, + channelId: backendChannelId, }, (output) => invalidateTasks(output.task), ); @@ -130,9 +170,12 @@ export function useGenerateFreeformCanvas(args: { const task = result.data.task; // File into the channel + record as the canvas's generation task. Both - // are best-effort: a failure here shouldn't undo a started task. + // are best-effort: a failure here shouldn't undo a started task. The + // generation-task write is awaited so a caller that navigates to the + // canvas right after generate() lands on the generating view (with the + // run in the side panel), not the empty hero. void fileTask(channelId, task.id, task.title).catch(() => {}); - void setGenerationTask(dashboardId, task.id).catch(() => {}); + await setGenerationTask(dashboardId, task.id).catch(() => {}); // Track this run so a toast (with a link back here) fires when it // finishes, even after the user navigates to another canvas. useCanvasGenerationTrackerStore @@ -148,7 +191,7 @@ export function useGenerateFreeformCanvas(args: { // who already named the canvas) leaves the existing title untouched. if (isPlaceholderCanvasName(name)) { void titleGenerator - .generateCanvasName(opts.instruction) + .generateCanvasName(instruction) .then(async (generated) => { const title = generated?.trim(); if (title) { @@ -178,11 +221,8 @@ export function useGenerateFreeformCanvas(args: { fileTask, setGenerationTask, renameDashboard, - dashboardId, channelId, - name, channelName, - templateId, channelContext, ], ); diff --git a/packages/ui/src/features/message-editor/components/ModeSelector.test.tsx b/packages/ui/src/features/message-editor/components/ModeSelector.test.tsx new file mode 100644 index 0000000000..c32b61c267 --- /dev/null +++ b/packages/ui/src/features/message-editor/components/ModeSelector.test.tsx @@ -0,0 +1,47 @@ +import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ModeSelector } from "./ModeSelector"; + +function modeOption(currentValue = "plan"): SessionConfigOption { + return { + id: "mode", + name: "Mode", + type: "select", + category: "mode", + currentValue, + options: [ + { value: "plan", name: "Plan" }, + { value: "auto", name: "Auto" }, + ], + } as SessionConfigOption; +} + +describe("ModeSelector", () => { + it("shows the current mode on the trigger", () => { + render( + , + ); + expect(screen.getByRole("button", { name: "Mode" })).toHaveTextContent( + "Plan", + ); + }); + + it("shows Canvas on the trigger while canvas mode is armed", () => { + render( + , + ); + const trigger = screen.getByRole("button", { name: "Mode" }); + expect(trigger).toHaveTextContent("Canvas"); + expect(trigger).not.toHaveTextContent("Plan"); + }); +}); diff --git a/packages/ui/src/features/message-editor/components/ModeSelector.tsx b/packages/ui/src/features/message-editor/components/ModeSelector.tsx index d3accc7d3a..f56a8466ae 100644 --- a/packages/ui/src/features/message-editor/components/ModeSelector.tsx +++ b/packages/ui/src/features/message-editor/components/ModeSelector.tsx @@ -1,5 +1,5 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; -import { CaretDown, ChartLineUp } from "@phosphor-icons/react"; +import { CaretDown, ChartLineUp, Shapes } from "@phosphor-icons/react"; import { Button, DropdownMenu, @@ -31,6 +31,16 @@ interface ModeSelectorProps { active: boolean; onToggle: () => void; }; + /** + * When provided, a "Canvas" toggle renders in the same trailing section + * (channels composer only). Arming it makes the next submit generate a + * canvas from the prompt instead of creating a plain task; while armed the + * trigger reads "Canvas" so the composer's state is visible at a glance. + */ + canvas?: { + active: boolean; + onToggle: () => void; + }; } export function ModeSelector({ @@ -39,10 +49,13 @@ export function ModeSelector({ allowBypassPermissions, disabled, autoresearch, + canvas, }: ModeSelectorProps) { const [open, setOpen] = useState(false); const pendingValueRef = useRef(null); - const pendingAutoresearchRef = useRef(false); + // A toggle picked from the menu, applied after the menu closes (like a mode + // change) so the composer doesn't relayout under the closing menu. + const pendingToggleRef = useRef<(() => void) | null>(null); const displayOption = useRetainedConfigOption(modeOption); if (!displayOption || displayOption.type !== "select") return null; @@ -63,9 +76,38 @@ export function ModeSelector({ if (options.length === 0) return null; const currentValue = displayOption.currentValue; - const currentStyle = getModeStyle(currentValue); - const currentLabel = - allOptions.find((opt) => opt.value === currentValue)?.name ?? currentValue; + const canvasActive = !!canvas?.active; + const currentStyle = canvasActive + ? { icon: , className: "text-teal-11" } + : getModeStyle(currentValue); + const currentLabel = canvasActive + ? "Canvas" + : (allOptions.find((opt) => opt.value === currentValue)?.name ?? + currentValue); + + const toggles: Array<{ + label: string; + active: boolean; + onToggle: () => void; + icon: React.ReactNode; + className: string; + }> = []; + if (canvas) { + toggles.push({ + label: "Canvas", + ...canvas, + icon: , + className: "text-teal-11", + }); + } + if (autoresearch) { + toggles.push({ + label: "Autoresearch", + ...autoresearch, + icon: , + className: "text-violet-11", + }); + } return ( Mode { pendingValueRef.current = value; setOpen(false); @@ -126,23 +171,20 @@ export function ModeSelector({ ); })} - {autoresearch && ( - <> - - { - pendingAutoresearchRef.current = true; - setOpen(false); - }} - > - - - - Autoresearch - - - )} + {toggles.length > 0 && } + {toggles.map((toggle) => ( + { + pendingToggleRef.current = toggle.onToggle; + setOpen(false); + }} + > + {toggle.icon} + {toggle.label} + + ))} ); diff --git a/packages/ui/src/features/message-editor/components/PromptInput.tsx b/packages/ui/src/features/message-editor/components/PromptInput.tsx index cf66715b4b..57044cf66c 100644 --- a/packages/ui/src/features/message-editor/components/PromptInput.tsx +++ b/packages/ui/src/features/message-editor/components/PromptInput.tsx @@ -48,6 +48,14 @@ export interface PromptInputProps { active: boolean; onToggle: () => void; }; + /** + * When provided, the mode dropdown gains a "Canvas" toggle (channels + * composer only). `active` drives its checkmark and the trigger label. + */ + canvas?: { + active: boolean; + onToggle: () => void; + }; // capabilities enableBashMode?: boolean; enableCommands?: boolean; @@ -103,6 +111,7 @@ export const PromptInput = forwardRef( onModeChange, allowBypassPermissions = false, autoresearch, + canvas, enableBashMode = false, enableCommands = true, modelSelector, @@ -405,6 +414,7 @@ export const PromptInput = forwardRef( allowBypassPermissions={allowBypassPermissions} disabled={disabled} autoresearch={autoresearch} + canvas={canvas} /> )} {modelSelector && {modelSelector}}