Files
app/frontend/src/components/Flow/CommandPalette.tsx
T
stroblmeandClaude Fable 5 fa12ffd323 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
2026-08-16 16:43:41 +02:00

134 lines
3.8 KiB
TypeScript

import { useQuery } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import type { FlowSummary, NodeTypeInfo } from "@/client"
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { libraryQueryOptions } from "./queries"
/**
* ⌘K: add a node, jump to another flow, or run the current one, without
* reaching for the dock.
*/
export function CommandPalette({
open,
onOpenChange,
nodeTypes,
flows,
onAddNode,
onAddSharedNode,
onRun,
}: {
open: boolean
onOpenChange: (open: boolean) => void
nodeTypes: NodeTypeInfo[]
flows: FlowSummary[]
onAddNode: (type: string) => void
onAddSharedNode: (libName: string) => void
onRun: () => void
}) {
const navigate = useNavigate()
const { data: libraryData } = useQuery({
...libraryQueryOptions(),
enabled: open,
})
// Picking an item re-renders the canvas, which can interrupt the dialog's
// exit animation and leave its overlay swallowing clicks. Unmounting the
// dialog outright is deterministic; the palette does not need to fade out.
if (!open) return null
const library = libraryData ?? []
const close = (action: () => void) => {
onOpenChange(false)
action()
}
return (
<CommandDialog
open={open}
onOpenChange={onOpenChange}
title="Commands"
description="Add a node, switch flow, or run"
>
<CommandInput placeholder="Add a node, switch flow, run…" />
<CommandList>
<CommandEmpty>Nothing matches that.</CommandEmpty>
<CommandGroup heading="Add a node">
{nodeTypes.map((nodeType) => (
<CommandItem
key={nodeType.type}
value={`${nodeType.title} ${nodeType.description}`}
onSelect={() => close(() => onAddNode(nodeType.type))}
>
<span className="flex flex-col">
<span>{nodeType.title}</span>
<span className="text-xs text-muted-foreground">
{nodeType.description}
</span>
</span>
</CommandItem>
))}
</CommandGroup>
{library.length > 0 ? (
<CommandGroup heading="Reusable nodes">
{library.map((entry) => (
<CommandItem
key={entry.name}
value={`shared ${entry.name}`}
onSelect={() => close(() => onAddSharedNode(entry.name))}
>
<span className="flex flex-col">
<span className="font-mono">{entry.name}</span>
<span className="text-xs text-muted-foreground">
{(entry.used_by ?? []).length === 1
? "Used once"
: `Used ${(entry.used_by ?? []).length} times`}
, code shared with every flow using it
</span>
</span>
</CommandItem>
))}
</CommandGroup>
) : null}
{flows.length > 0 ? (
<CommandGroup heading="Flows">
{flows.map((flow) => (
<CommandItem
key={flow.name}
value={`flow ${flow.name} ${flow.title}`}
onSelect={() =>
close(() =>
navigate({
to: "/flows/$flowName",
params: { flowName: flow.name },
}),
)
}
>
{flow.title || flow.name}
</CommandItem>
))}
</CommandGroup>
) : null}
<CommandGroup heading="Actions">
<CommandItem value="run flow" onSelect={() => close(onRun)}>
Run this flow
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
)
}