Skip to content

improvement(workflow-panel): header tooltips, search shortcut hint, URL-driven active tab#4523

Open
stylessh wants to merge 2 commits intosimstudioai:stagingfrom
stylessh:stylessh/misc-changes-workflow-panel
Open

improvement(workflow-panel): header tooltips, search shortcut hint, URL-driven active tab#4523
stylessh wants to merge 2 commits intosimstudioai:stagingfrom
stylessh:stylessh/misc-changes-workflow-panel

Conversation

@stylessh
Copy link
Copy Markdown
Contributor

@stylessh stylessh commented May 8, 2026

Summary

A handful of polish changes on the workflow page's right-side panel:

  • Toolbar header tooltips — added Info icons next to the Triggers and Blocks labels with concise definitions ("Events that start a workflow." / "Actions that make up the steps of a workflow."). The Blocks icon stops click propagation so the section's collapse toggle isn't triggered when hovering the icon.
  • Search shortcut badge — rendered a kbd-style badge next to the toolbar search icon showing the existing Mod+Alt+F shortcut. Uses lucide Option and Command icons on Mac, falls back to Ctrl+Alt+F text on Windows/Linux. The badge waits for client mount before rendering so the wrong platform doesn't flash.
  • Active panel tab → URL — moved the panel's activeTab from the persisted Zustand store into the URL via nuqs (?panel=copilot|toolbar|editor). The blocking script in app/layout.tsx now reads ?panel= instead of localStorage, so a hard refresh paints the correct tab before React hydrates — fixes the flash where Copilot rendered briefly before swapping to the previously-selected tab.

nuqs is added to apps/sim at 2.8.9 to match the install in #4522, and NuqsAdapter is wired in app/layout.tsx in the same position (inside QueryProvider, outside SessionProvider) so the two PRs don't conflict.

The editor store's old usePanelStore.getState().setActiveTab('editor') call (fired from outside React when a block is selected) goes through a new panel:set-tab CustomEvent that the panel component listens for, since nuqs state can only be updated from inside React.

Test plan

  • Hard refresh the workflow page with the toolbar or editor tab previously active → no Copilot flash; the correct tab paints immediately.
  • Switch tabs → URL updates with ?panel=... (or clears when on the default Copilot tab).
  • Click a block on the canvas → editor tab opens (event-driven path).
  • Press ⌘⌥F / Ctrl+Alt+F → toolbar tab focuses and the search input opens.
  • Hover the Info icons in the toolbar → tooltips appear with the right copy.
  • Confirm the kbd badge next to the search icon does not flash with the wrong platform variant on first load.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

@stylessh is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 8, 2026

PR Summary

Medium Risk
Changes how the workflow panel’s active tab state is stored and restored (from persisted Zustand to URL query state), which can affect navigation, hydration behavior, and cross-component tab switching. Also introduces a new dependency (nuqs) and a new event-based bridge for non-React store code to request tab changes.

Overview
The workflow right-side panel now stores the active tab in the URL (?panel=copilot|toolbar|editor) via nuqs, and the pre-hydration blocking script in layout.tsx reads this query param to set data-panel-active-tab so the correct tab paints on hard refresh (avoiding the Copilot flash). The persisted usePanelStore no longer tracks activeTab and now only persists panelWidth; related logic that previously toggled tabs via the store is updated to use URL state.

To support tab changes initiated outside React, the editor store dispatches a panel:set-tab window event and the Panel listens to apply it. The toolbar UI is polished with Triggers/Blocks info tooltips and a platform-aware keyboard shortcut hint next to the search button, plus NuqsAdapter is added to the app layout and nuqs is added as a dependency.

Reviewed by Cursor Bugbot for commit 6848b92. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR polishes the workflow panel's right side by migrating the active tab from Zustand's persisted store to URL state via nuqs, adding toolbar header tooltips with Info icons, and surfacing the Mod+Alt+F search shortcut as a kbd badge. The URL-driven tab eliminates the hydration flash where Copilot briefly rendered before the correct tab was applied on hard refresh.

  • URL-driven active tab: ?panel=copilot|toolbar|editor is now the source of truth; the blocking script in layout.tsx reads the same param to set data-panel-active-tab before paint, and nuqs keeps it in sync thereafter. Tab changes from outside React (e.g. block clicks) go through a panel:set-tab CustomEvent.
  • Toolbar UI additions: Info tooltips on the Triggers/Blocks section headers and a platform-aware kbd badge (⌥⌘F / Ctrl+Alt+F) next to the search icon, rendered only after client mount to prevent SSR platform mismatch flash.

Confidence Score: 4/5

The core tab-flash fix is well-designed and the CustomEvent bridge handles the nuqs/store boundary cleanly. The main area to watch is use-block-visual.ts — every block now subscribes to URL changes, which is a regression from the previous targeted Zustand selector on large workflows.

The URL-driven tab approach is sound and the blocking-script/hydration handoff is carefully sequenced. The only functional concern is that migrating the panel tab check into useQueryState inside use-block-visual.ts makes all workflow blocks re-render on every tab switch, whereas the old Zustand selector short-circuited for non-relevant blocks. For typical workflow sizes this is unlikely to be noticeable, but it could surface on larger workflows.

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts — the useQueryState call here is the main spot to revisit for render performance on large workflows.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts Now calls useQueryState('panel', ...) for every block on the canvas, subscribing all blocks to URL changes. Duplicates the PANEL_TABS constant that also lives in panel.tsx.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx Active tab migrated cleanly to nuqs; panel:set-tab CustomEvent bridge for store-driven tab switches is well-scoped. PANEL_TABS constant is duplicated here and in use-block-visual.ts.
apps/sim/stores/panel/editor/store.ts Replaced direct usePanelStore.getState().setActiveTab calls with a requestEditorTab() helper that dispatches a panel:set-tab CustomEvent; correctly guards against SSR with a typeof window check.
apps/sim/stores/panel/store.ts activeTab / setActiveTab removed cleanly; partialize now explicitly persists only panelWidth, preventing accidental future state leakage into localStorage.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx Adds tooltip icons and keyboard shortcut badge. Platform detection deferred to useEffect to avoid SSR flash; click propagation on the Blocks Info icon is stopped correctly.
apps/sim/app/layout.tsx Blocking script updated to read ?panel= instead of localStorage; always sets a default of copilot on the data-panel-active-tab attribute. NuqsAdapter wired inside QueryProvider.
apps/sim/stores/panel/types.ts activeTab and setActiveTab removed from PanelState; PanelTab type is retained for consumers.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant BlockingScript as Blocking Script (layout.tsx)
    participant NuqsAdapter
    participant Panel
    participant EditorStore as usePanelEditorStore
    participant BlockVisual as useBlockVisual (each block)

    Browser->>BlockingScript: "Hard refresh /?panel=editor"
    BlockingScript->>Browser: setAttribute('data-panel-active-tab', 'editor')
    Note over Browser: CSS hides non-editor tabs before React hydrates

    Browser->>NuqsAdapter: React hydrates
    NuqsAdapter->>Panel: useQueryState('panel') → 'editor'
    Panel->>Browser: removeAttribute('data-panel-active-tab')
    Note over Browser: React now owns tab visibility

    Browser->>EditorStore: User clicks a block
    EditorStore->>EditorStore: setCurrentBlockId(blockId)
    EditorStore->>Browser: dispatchEvent('panel:set-tab', 'editor')
    Browser->>Panel: CustomEvent handler fires
    Panel->>NuqsAdapter: setActiveTabRaw('editor')
    NuqsAdapter->>Browser: "URL updated → ?panel=editor"

    NuqsAdapter->>Panel: "re-render (activeTab = 'editor')"
    NuqsAdapter->>BlockVisual: "re-render × N blocks (panelTab = 'editor')"
Loading

Reviews (1): Last reviewed commit: "remove redundant data attribute cleanup ..." | Re-trigger Greptile

Comment on lines +60 to +61
const [panelTab] = useQueryState('panel', parseAsStringLiteral(PANEL_TABS).withDefault('copilot'))
const isEditorOpen = !isPreview && !isEmbedded && isThisBlockInEditor && panelTab === 'editor'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 All blocks re-render on every panel tab switch

useQueryState hooks are notified via React context whenever the ?panel= URL param changes, so every block on the canvas will re-render each time the user switches tabs. The previous Zustand selector approach short-circuited early — if (isPreview || isEmbedded || !isThisBlockInEditor) return false — so Zustand never subscribed those blocks to activeTab mutations. For workflows with many blocks, switching tabs will now trigger N extra renders instead of at most 1. Consider moving the panelTab === 'editor' check back into the usePanelEditorStore selector (e.g. adding activeTab back to the store as a non-persisted field, or deriving it from the URL once in the Panel and propagating it down via a lightweight context).

Comment on lines +11 to 14
const PANEL_TABS = ['copilot', 'toolbar', 'editor'] as const

/**
* Props for the useBlockVisual hook.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 PANEL_TABS constant is duplicated across files

The same ['copilot', 'toolbar', 'editor'] as const array is defined independently in both use-block-visual.ts and panel.tsx. PanelTab is already exported from stores/panel/types.ts, so the backing tuple could live there too, giving a single source of truth for the literal union.

Suggested change
const PANEL_TABS = ['copilot', 'toolbar', 'editor'] as const
/**
* Props for the useBlockVisual hook.
// Consider exporting PANEL_TABS from '@/stores/panel/types' and importing it here
// to avoid duplicating the constant that also lives in panel.tsx.
const PANEL_TABS = ['copilot', 'toolbar', 'editor'] as const
/**
* Props for the useBlockVisual hook.

Comment on lines +473 to +482
useEffect(() => {
const handler = (e: Event) => {
const tab = (e as CustomEvent<PanelTab>).detail
if (tab === 'copilot' || tab === 'toolbar' || tab === 'editor') {
setActiveTab(tab)
}
}
window.addEventListener('panel:set-tab', handler)
return () => window.removeEventListener('panel:set-tab', handler)
}, [setActiveTab])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 panel:set-tab event is fire-and-forget — missed events leave the tab unchanged

requestEditorTab() dispatches a synchronous CustomEvent on window, and the Panel only processes it while mounted with an active listener. If the Panel is mid-unmount/remount (e.g. during a fast route transition followed by an immediate block click), the event fires into a gap where no listener is registered and the URL never updates — the editor tab silently fails to open. A safety net like checking activeTab after a microtask, or using a small shared atom/signal instead of a one-shot event, would make the bridge more resilient.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6848b92. Configure here.

import { usePanelEditorStore } from '@/stores/panel'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

const PANEL_TABS = ['copilot', 'toolbar', 'editor'] as const
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated PANEL_TABS array risks silent desynchronization

Low Severity

The PANEL_TABS constant is independently defined in both use-block-visual.ts and panel.tsx. Both must stay in sync with the PanelTab type in @/stores/panel/types.ts, which already serves as the source of truth for valid tab values. If a tab is added or renamed, only one array might get updated, causing the nuqs parser in the other file to silently reject the new value and fall back to 'copilot'. Extracting a single shared PANEL_TABS array next to the PanelTab type would eliminate this drift risk.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6848b92. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant