Copy nodes, bind the keys, and keep publish within reach

The floating bars used to make way for a side panel, which hid Publish
exactly when a node had just been edited. They keep their lane now and
the panel is inset out of it; the enlarged code editor stays a floating
surface between the two bars rather than covering them.

Shortcuts are one small registry (`lib/shortcuts.ts`): undo, redo, ⌘K,
copy, paste, and ⌘S — which publishes the flow, or applies the code when
the editor has focus. Copying carries a node's own source through
localStorage, so a paste works in another flow too.

A flow is now named where a node is, at the top of its panel and only
there, and both take effect on Confirm. Flow-level edits go through the
undo stack with everything else, clicking the canvas puts the flow panel
away, a failing node opens the logs filtered to its own traceback, and
the panel carries its test id on a phone as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
This commit is contained in:
2026-08-16 16:43:41 +02:00
co-authored by Claude Fable 5
parent 929bb56662
commit fa12ffd323
9 changed files with 477 additions and 222 deletions
+28 -1
View File
@@ -2,6 +2,7 @@ import { Handle, type NodeProps, Position } from "@xyflow/react"
import {
Bell,
Braces,
Bug,
Clock,
Code2,
Database,
@@ -20,6 +21,7 @@ import {
import { memo } from "react"
import type { MessageSpec, NodeDef_Input } from "@/client"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
@@ -61,6 +63,8 @@ export type FlowNodeData = {
typeLabel: string
isPlugin?: boolean
issueText: string
/** Open the logs at this node's own lines. */
onShowLogs?: (nodeId: string) => void
[key: string]: unknown
}
@@ -103,7 +107,7 @@ function PortHandles({
}
function FlowNodeComponent({ data, selected }: NodeProps) {
const { definition, flow, typeLabel, isPlugin, issueText } =
const { definition, flow, typeLabel, isPlugin, issueText, onShowLogs } =
data as FlowNodeData
const live = useNodeStatus(`${flow}.${definition.id}`)
const emits = useNodeEmits(`${flow}.${definition.id}`)
@@ -153,6 +157,29 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
{typeLabel}
</span>
</span>
{status === "error" && onShowLogs ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
// `nodrag` keeps xyflow from reading the press as a drag; the
// click itself is stopped so the node panel stays closed.
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
aria-label="Show what this node printed"
data-testid="node-traceback"
onClick={(event) => {
event.stopPropagation()
onShowLogs(definition.id)
}}
>
<Bug />
</Button>
</TooltipTrigger>
<TooltipContent>Show the traceback</TooltipContent>
</Tooltip>
) : null}
{style ? (
<Tooltip>
<TooltipTrigger asChild>