From 6b9e5ea4567b0cecb59e82a75f43fe2c5d5cf1b5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 21 Aug 2026 20:46:32 -0700 Subject: [PATCH 1/4] improvement(menus): one separator per menu, before the destructive action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Menus banded themselves into semantic groups — navigation, status, edit, copy, destructive — behind two to four separators each. No toolbar in the app renders a divider: every header is a flat gap-1 chip row and every bulk action bar a flat gap-[5px] run. The bands therefore taught a taxonomy the user met on no other surface, and because each band is conditional, the same action landed in a different group depending on which siblings happened to be visible. Pin sat alone in one caller of the shared workflow menu and beside Duplicate in another. Every menu now carries at most one rule, immediately before the destructive group. Order is untouched, so the toolbar-mirroring the ordering rule requires is unaffected. Two separator bugs fixed. The logs row menu had two unconditional separators above conditional items, so a log already filtered by its workflow with no active filters ended on a dangling rule. The shared workflow menu guarded its destructive rule on showLeave alone while the Leave item required showLeave && onLeave, so a caller passing showLeave from a permission check with a conditional onLeave would trail a rule under the last item; every term in both guards is now the exact render condition of the item it stands for. Removes groupNonDestructiveActions and separateNavigationAction. Between them they moved one separator for one caller, four of six branches were unreachable, and separateNavigationAction had no observable effect anywhere in the repo. The separator matrix was previously untested, which is how the showLeave asymmetry survived; it now has invariants including a flag sweep. --- .claude/rules/sim-list-ordering.md | 48 ++++++++ CLAUDE.md | 4 +- .../file-row-context-menu.tsx | 12 +- .../browser-session/browser-tab-strip.tsx | 2 - .../chunk-context-menu/chunk-context-menu.tsx | 13 +- .../document-context-menu.tsx | 13 +- .../knowledge-base-context-menu.tsx | 10 +- .../log-row-context-menu.tsx | 27 ++-- .../components/context-menu/context-menu.tsx | 11 +- .../headers/workflow-group-meta-cell.tsx | 53 ++++---- .../table-context-menu/table-context-menu.tsx | 11 +- .../context-menu/context-menu.test.tsx | 115 ++++++++++++++++++ .../components/context-menu/context-menu.tsx | 64 +++++----- 13 files changed, 263 insertions(+), 120 deletions(-) diff --git a/.claude/rules/sim-list-ordering.md b/.claude/rules/sim-list-ordering.md index 2966eb4a1a6..ad766adced5 100644 --- a/.claude/rules/sim-list-ordering.md +++ b/.claude/rules/sim-list-ordering.md @@ -26,6 +26,54 @@ Left-to-right becomes top-to-bottom. A toolbar reading `Filter · Sort · Export Platform-only entries (desktop **Browser** and **Terminal**) trail the shared set rather than interleaving, so the common prefix is identical on every platform. +## Grouping: one rule, before the destructive action + +Order is governed above. **Separators are governed here** — and the answer is: use at most one. + +Put a single `DropdownMenuSeparator` immediately before the destructive group (Delete, Leave, +Close, Hide) and nowhere else. Everything above it runs uninterrupted in toolbar-mirroring order. + +```tsx +// ✗ Bad — four semantic bands the user meets nowhere else +Open in new tab │─── Rename, Lock │─── Duplicate, Export │─── Delete + +// ✓ Good — one rule, isolating the irreversible action +Open in new tab, Rename, Lock, Duplicate, Export │─── Delete +``` + +**Why one.** No toolbar in this app renders a divider — every header is a flat +`HEADER_ACTION_CLUSTER` (`gap-1`) chip row and every bulk action bar a flat `gap-[5px]` run. A +menu banded into navigation / status / edit / copy / destructive therefore teaches a taxonomy +that appears on no other surface, and because each band is conditional, the same action lands in +a different group depending on which sibling items happen to be visible. The one thing a rule +genuinely buys is a stop before the action you cannot undo. + +A second rule is justified only when a menu mixes genuinely different *scopes* — cell-level and +table-level actions in one menu, say — not different verbs. + +**Both sides of every rule must be guaranteed non-empty.** Write the separator's guard out of +the *exact* render conditions of the items around it, never a looser approximation: + +```tsx +// ✗ Bad — `showLeave` alone, while the Leave item needs `showLeave && onLeave`. +// A caller passing showLeave from a permission check with a conditional +// onLeave renders a trailing rule under the last item. +{hasActionsAbove && (showLeave || showDelete) && } + +// ✓ Good — each term is the item's own condition, verbatim +const hasDestructiveSection = (showLeave && onLeave) || showDelete +{hasActionsAboveDestructive && hasDestructiveSection && } +``` + +This is the failure that put a dangling rule at the bottom of the logs row menu, where two +unconditional separators sat above conditional items. + +**Do not add a prop to move a rule.** The shared workflow context menu grew +`groupNonDestructiveActions` and `separateNavigationAction` for this; between them they moved one +separator for one caller, four of six branches were unreachable, and `separateNavigationAction` +had no observable effect anywhere in the repo. Both are gone. A menu that wants different +grouping wants the standard grouping. + ## Encode the order once An order duplicated across surfaces is an order that will drift. Export **one** constant and sort by it — do not hand-maintain a matching literal per menu. diff --git a/CLAUDE.md b/CLAUDE.md index 60c0990eecc..bc6b1ce36ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -386,7 +386,9 @@ Co-locate a `search-params.ts` per feature exporting the parser map (single sour A list orders itself the way the user already reads the same things somewhere else. Resource menus (`+` attach, `@` mention, resource-tab `+`) mirror the **sidebar** top-down; a row or root **context menu** mirrors that surface's **toolbar**, left-to-right becoming top-to-bottom; tab strips mirror their nav. Platform-only entries (desktop Browser, Terminal) trail the shared set. -Encode the order in ONE exported constant and sort by it — never a hand-maintained literal per menu (`RESOURCE_MENU_ORDER` / `byResourceMenuOrder` in `home/components/mothership-view/components/resource-registry`). Render mixed item kinds in a single ordered pass; emitting all submenu-backed families and then all flat ones silently pins every submenu to the top no matter what the constant says. Divergence is allowed only for search ranking, user-controlled ordering, and recency. Full rule in `.claude/rules/sim-list-ordering.md`. +Encode the order in ONE exported constant and sort by it — never a hand-maintained literal per menu (`RESOURCE_MENU_ORDER` / `byResourceMenuOrder` in `home/components/mothership-view/components/resource-registry`). Render mixed item kinds in a single ordered pass; emitting all submenu-backed families and then all flat ones silently pins every submenu to the top no matter what the constant says. Divergence is allowed only for search ranking, user-controlled ordering, and recency. + +**Grouping**: at most ONE `DropdownMenuSeparator` per menu, immediately before the destructive group (Delete/Leave/Close/Hide). No toolbar in the app renders a divider, so multi-band menus teach a taxonomy that exists on no other surface. Build each separator's guard from the EXACT render conditions of the items on both sides — a looser guard is what leaves a dangling rule when its group is conditional. Never add a prop to move a rule. Full rule in `.claude/rules/sim-list-ordering.md`. ## Styling diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index 1fda2da80d7..e3706877b81 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -55,6 +55,16 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ }: FileRowContextMenuProps) { const isMultiSelect = selectedCount > 1 + /** + * Everything that can render above `Delete`: Open/Pin need a single selection, + * Download needs its handler, and the edit trio needs `canEdit` — so a multi-select + * with no download and only a move target leaves `Move to` alone above the rule. + * + * @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. + */ + const hasActionsAboveDestructive = + !isMultiSelect || !!onDownload || (!!onMove && !!moveOptions && moveOptions.length > 0) + return ( !open && onClose()} modal={false}> @@ -91,7 +101,6 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ )} {canEdit && ( <> - {!isMultiSelect && ( @@ -120,6 +129,7 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ )} + {hasActionsAboveDestructive && } {isMultiSelect ? `Delete ${selectedCount} items` : 'Delete'} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-strip.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-strip.tsx index df5adc320c8..2945162eac1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-strip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-tab-strip.tsx @@ -216,9 +216,7 @@ export function BrowserTabStrip({ onOpenInNewTab={openTabInExternalBrowser} openInNewTabLabel='Open in External Browser' openInNewTabPosition='last' - separateNavigationAction showOpenInNewTab={Boolean(contextTab?.url && contextTab.url !== 'about:blank')} - groupNonDestructiveActions onTogglePin={ contextTab ? () => onSetTabPinned(contextTab.tabId, !contextTab.pinned) : undefined } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index 605dfa1f53a..d03d8fa42d7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -71,6 +71,8 @@ export function ChunkContextMenu({ const hasEditSection = !isMultiSelect && (!!onEdit || !!onCopyContent) const hasStateSection = !!onToggleEnabled const hasDestructiveSection = !!onDelete + /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ + const hasActionsAboveDestructive = hasNavigationSection || hasEditSection || hasStateSection return ( !open && onClose()} modal={false}> @@ -102,11 +104,6 @@ export function ChunkContextMenu({ Open in new tab )} - {hasNavigationSection && - (hasEditSection || hasStateSection || hasDestructiveSection) && ( - - )} - {!isMultiSelect && onEdit && ( @@ -119,10 +116,6 @@ export function ChunkContextMenu({ Copy content )} - {hasEditSection && (hasStateSection || hasDestructiveSection) && ( - - )} - {onToggleEnabled && ( @@ -130,7 +123,7 @@ export function ChunkContextMenu({ )} - {hasStateSection && hasDestructiveSection && } + {hasActionsAboveDestructive && hasDestructiveSection && } {onDelete && ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index 7050da64725..bb8fe70e9ca 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -71,6 +71,8 @@ export function DocumentContextMenu({ const hasEditSection = !isMultiSelect && (!!onRename || !!onViewTags) const hasStateSection = !!onToggleEnabled const hasDestructiveSection = !!onDelete + /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ + const hasActionsAboveDestructive = hasNavigationSection || hasEditSection || hasStateSection return ( !open && onClose()} modal={false}> @@ -108,11 +110,6 @@ export function DocumentContextMenu({ Open source )} - {hasNavigationSection && - (hasEditSection || hasStateSection || hasDestructiveSection) && ( - - )} - {!isMultiSelect && onRename && ( @@ -125,10 +122,6 @@ export function DocumentContextMenu({ Tags )} - {hasEditSection && (hasStateSection || hasDestructiveSection) && ( - - )} - {onToggleEnabled && ( @@ -136,7 +129,7 @@ export function DocumentContextMenu({ )} - {hasStateSection && hasDestructiveSection && } + {hasActionsAboveDestructive && hasDestructiveSection && } {onDelete && ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx index b809950a158..f815043a66c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx @@ -75,6 +75,8 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ const hasMoveSection = !disableEdit && !!onMove && !!moveOptions && moveOptions.length > 0 const hasEditSection = (showEdit && !!onEdit) || hasMoveSection const hasDestructiveSection = showDelete && !!onDelete + /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ + const hasActionsAboveDestructive = hasNavigationSection || hasInfoSection || hasEditSection return ( !open && onClose()} modal={false}> @@ -104,10 +106,6 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ Open in new tab )} - {hasNavigationSection && (hasInfoSection || hasEditSection || hasDestructiveSection) && ( - - )} - {showViewTags && onViewTags && ( @@ -126,8 +124,6 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ {pinned ? 'Unpin' : 'Pin'} )} - {hasInfoSection && (hasEditSection || hasDestructiveSection) && } - {showEdit && onEdit && ( @@ -147,7 +143,7 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ )} - {hasEditSection && hasDestructiveSection && } + {hasActionsAboveDestructive && hasDestructiveSection && } {showDelete && onDelete && ( diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx index 4f52bd9ff65..c24c7029a57 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx @@ -103,23 +103,18 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ onCloseAutoFocus={(e) => e.preventDefault()} > {isRetryable && ( - <> - - - {isRetryPending ? 'Retrying...' : 'Retry'} - - - + + + {isRetryPending ? 'Retrying...' : 'Retry'} + )} {showCancelAction && ( - <> - - - {isStopping ? 'Stopping…' : 'Cancel Run'} - - - + + + {isStopping ? 'Stopping…' : 'Cancel Run'} + )} + {(isRetryable || showCancelAction) && } Copy Run ID @@ -128,8 +123,6 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ Copy Link - - Open Workflow @@ -138,8 +131,6 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({ Open Snapshot - - {!isFilteredByThisWorkflow && ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu/context-menu.tsx index 1b5ebe5ce06..4b1395afd32 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu/context-menu.tsx @@ -169,13 +169,10 @@ export function ContextMenu({ onCloseAutoFocus={(e) => e.preventDefault()} > {onAddToChat && ( - <> - - - {addToChatLabel} - - - + + + {addToChatLabel} + )} {contextMenu.columnName && canEditCell && ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 68ee553cdad..faa5cf0c112 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -160,34 +160,31 @@ export function ColumnOptionsMenu({ onCloseAutoFocus={(e) => e.preventDefault()} > {showRunActions && ( - <> - - - - Run - - - {showRunSelected && ( - onRunColumnSelected?.()}> - {`Run ${selectedRowCount} selected ${selectedRowCount === 1 ? 'row' : 'rows'}`} - - )} - onRunColumnAll?.()}> - {runLabels.all} - - onRunColumnIncomplete?.()}> - {runLabels.incomplete} + + + + Run + + + {showRunSelected && ( + onRunColumnSelected?.()}> + {`Run ${selectedRowCount} selected ${selectedRowCount === 1 ? 'row' : 'rows'}`} - {onRunColumnLimited && - LIMITED_RUN_PRESETS.map((max) => ( - onRunColumnLimited(max)}> - {runLabels.limited(max)} - - ))} - - - - + )} + onRunColumnAll?.()}> + {runLabels.all} + + onRunColumnIncomplete?.()}> + {runLabels.incomplete} + + {onRunColumnLimited && + LIMITED_RUN_PRESETS.map((max) => ( + onRunColumnLimited(max)}> + {runLabels.limited(max)} + + ))} + + )} {/* Sort leads the column-scoped block: the options bar reads Filter · Sort · Columns, and this menu carries no Filter item, so Sort is the @@ -217,7 +214,6 @@ export function ColumnOptionsMenu({ Sort descending - )} {onViewWorkflow && ( @@ -236,7 +232,6 @@ export function ColumnOptionsMenu({ {isPinned ? 'Unpin column' : 'Pin column'} )} - onInsertLeft(column.key)}> Insert column left diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx index c5360fec673..0097cb1c3d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx @@ -114,8 +114,6 @@ export function TableContextMenu({ )} - {(onViewSchema || onRename || onImportCsv || onExportCsv || onMove) && - (onCopyId || onTogglePin || onDelete) && } {onTogglePin && ( @@ -128,7 +126,14 @@ export function TableContextMenu({ Copy ID )} - {(onCopyId || onTogglePin) && onDelete && } + {(onViewSchema || + onRename || + onImportCsv || + onExportCsv || + onMove || + onCopyId || + onTogglePin) && + onDelete && } {onDelete && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx index 47bea5f08c4..f7dd102d4c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx @@ -137,3 +137,118 @@ describe('sidebar context menu dismissal', () => { expect(onClose).toHaveBeenCalled() }) }) + +/** + * Separator invariants. The menu carries exactly one rule, immediately before the + * destructive group, and it may never render with an empty group on either side — + * see the grouping section of `.claude/rules/sim-list-ordering.md`. + * + * These pin the shape the flag matrix used to get wrong: `showLeave` in the rule's + * guard without the `&& onLeave` its item requires produced a trailing rule under + * the last item, and nothing covered it. + */ +describe('separators', () => { + function renderWith(props: Partial>) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render( + {}} + onDelete={() => {}} + showRename={false} + showDuplicate={false} + {...props} + /> + ) + ) + } + + /** Menu children in render order, each as 'sep' or its label. */ + function menuShape(): string[] { + const content = document.querySelector('[role="menu"]') + if (!content) return [] + return Array.from(content.children).map((el) => + el.getAttribute('role') === 'separator' ? 'sep' : (el.textContent ?? '') + ) + } + + it('puts its one rule directly before the destructive action', () => { + renderWith({ showRename: true, onRename: () => {}, showDelete: true }) + const shape = menuShape() + expect(shape.filter((s) => s === 'sep')).toHaveLength(1) + expect(shape[shape.indexOf('sep') + 1]).toBe('Delete') + }) + + it('renders no rule when nothing precedes the destructive action', () => { + renderWith({ showDelete: true }) + expect(menuShape()).toEqual(['Delete']) + }) + + it('renders no rule when there is no destructive action', () => { + renderWith({ showRename: true, onRename: () => {}, showDelete: false }) + const shape = menuShape() + expect(shape).not.toContain('sep') + expect(shape).toEqual(['Rename']) + }) + + it('never trails a rule when showLeave is set without an onLeave handler', () => { + renderWith({ + showRename: true, + onRename: () => {}, + showLeave: true, + onLeave: undefined, + showDelete: false, + }) + const shape = menuShape() + expect(shape.at(-1)).not.toBe('sep') + expect(shape).toEqual(['Rename']) + }) + + it('draws the rule for Leave when its handler is present', () => { + renderWith({ + showRename: true, + onRename: () => {}, + showLeave: true, + onLeave: () => {}, + showDelete: false, + }) + expect(menuShape()).toEqual(['Rename', 'sep', 'Leave']) + }) + + it('never renders two rules back to back across a broad flag sweep', () => { + const noop = () => {} + for (const showOpenInNewTab of [true, false]) { + for (const showPin of [true, false]) { + for (const showLock of [true, false]) { + for (const showExport of [true, false]) { + for (const showDelete of [true, false]) { + act(() => root?.unmount()) + container?.remove() + renderWith({ + showOpenInNewTab, + onOpenInNewTab: noop, + showPin, + onTogglePin: noop, + showLock, + onToggleLock: noop, + showExport, + onExport: noop, + showDelete, + }) + const shape = menuShape() + expect(shape.filter((s) => s === 'sep').length).toBeLessThanOrEqual(1) + expect(shape.at(0)).not.toBe('sep') + expect(shape.at(-1)).not.toBe('sep') + } + } + } + } + } + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 8be1feef7f3..aa623fa9d84 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -35,8 +35,6 @@ interface ContextMenuProps { onOpenInNewTab?: () => void openInNewTabLabel?: string openInNewTabPosition?: 'first' | 'last' - separateNavigationAction?: boolean - groupNonDestructiveActions?: boolean onMarkAsRead?: () => void onMarkAsUnread?: () => void onTogglePin?: () => void @@ -121,8 +119,6 @@ export function ContextMenu({ onOpenInNewTab, openInNewTabLabel = 'Open in new tab', openInNewTabPosition = 'first', - separateNavigationAction = false, - groupNonDestructiveActions = false, onMarkAsRead, onMarkAsUnread, onTogglePin, @@ -169,18 +165,43 @@ export function ContextMenu({ showUploadLogo = false, disableUploadLogo = false, }: ContextMenuProps) { - const hasNavigationSection = showOpenInNewTab && onOpenInNewTab - const hasStatusSection = + /** + * One rule, immediately before the destructive group — see the menu-grouping + * section of `.claude/rules/sim-list-ordering.md`. + * + * This menu previously carried four semantic bands (navigation / status / edit / + * copy / destructive) behind up to five separators. No toolbar in the app renders + * a divider — every header is a flat `gap-1` chip row — so those bands taught a + * taxonomy the user met nowhere else, and each caller's flag combination banded + * the same action differently (Pin alone here, Pin beside Duplicate there). Order + * still mirrors the surface's toolbar, which is what the ordering rule actually + * requires; only the rules between groups are gone. + * + * Every term below is the exact render condition of the item it stands for, so a + * separator can never outlive the group on either side of it. `showLeave` was the + * one asymmetric term — it omitted `&& onLeave`, so a caller passing `showLeave` + * from a permission check with a conditional `onLeave` (the `x ? fn : undefined` + * shape used for `onDuplicate`/`onTogglePin`/`onCloseTab` elsewhere) would have + * rendered a trailing rule under the last item. + */ + const hasActionsAboveDestructive = + (showOpenInNewTab && onOpenInNewTab) || (showMarkAsRead && onMarkAsRead) || (showMarkAsUnread && onMarkAsUnread) || - (showPin && onTogglePin) - const hasEditSection = + (showPin && onTogglePin) || (showRename && onRename) || (showCreate && onCreate) || (showCreateFolder && onCreateFolder) || (showLock && onToggleLock) || - (showUploadLogo && onUploadLogo) - const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport) + (showUploadLogo && onUploadLogo) || + (showDuplicate && onDuplicate) || + (showExport && onExport) + const hasDestructiveSection = + (showLeave && onLeave) || + showDelete || + (showCloseTab && onCloseTab) || + onCloseOtherTabs || + onCloseTabsToRight /** * Only the "Rename" item should trigger the `onCloseAutoFocus` refocus below — @@ -237,11 +258,6 @@ export function ContextMenu({ {openInNewTabLabel} )} - {openInNewTabPosition === 'first' && - (!groupNonDestructiveActions || separateNavigationAction) && - hasNavigationSection && - (hasStatusSection || hasEditSection || hasCopySection) && } - {showMarkAsRead && onMarkAsRead && ( )} - {!groupNonDestructiveActions && hasStatusSection && (hasEditSection || hasCopySection) && ( - - )} - {showRename && onRename && ( )} - {!groupNonDestructiveActions && hasEditSection && hasCopySection && ( - - )} {showDuplicate && onDuplicate && ( )} - {openInNewTabPosition === 'last' && - (!groupNonDestructiveActions || separateNavigationAction) && - hasNavigationSection && - (hasStatusSection || hasEditSection || hasCopySection) && } {openInNewTabPosition === 'last' && showOpenInNewTab && onOpenInNewTab && ( { @@ -386,12 +391,7 @@ export function ContextMenu({ )} - {(hasNavigationSection || hasStatusSection || hasEditSection || hasCopySection) && - (showLeave || - showDelete || - (showCloseTab && onCloseTab) || - onCloseOtherTabs || - onCloseTabsToRight) && } + {hasActionsAboveDestructive && hasDestructiveSection && } {showLeave && onLeave && ( Date: Fri, 21 Aug 2026 20:56:17 -0700 Subject: [PATCH 2/4] fix(menus): build every separator guard from its items' exact conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review caught two places where the grouping rule and the code disagreed. The tables row menu guarded its rule on `onMove` while the Move submenu needs a non-empty `moveOptions`, so a table whose other actions were all absent and whose move list was empty would draw the rule with nothing above it — the exact looseness the rule warns about. The logs row menu puts its one rule after Retry and Cancel Run rather than before a destructive group, which the rule as written did not cover. Retry is the primary action on a failed run and belongs at the top; the rule now describes the separator as fencing the consequential group at whichever end it sits, and names the logs menu as the one place that group leads. Also aligns three empty-space menu labels with the header chips they mirror: "Add document" and "Create chunk" were the only create actions not matching their toolbar, and the files menu said "Upload file" where its header says "Upload". Run order in the two column run menus now matches the action bar and the row menu — incomplete before all, not all before incomplete. --- .claude/rules/sim-list-ordering.md | 14 +++++++--- CLAUDE.md | 2 +- .../files-list-context-menu.tsx | 2 +- .../chunk-context-menu/chunk-context-menu.tsx | 4 +-- .../document-context-menu.tsx | 2 +- .../headers/workflow-group-meta-cell.tsx | 14 +++++++--- .../table-context-menu/table-context-menu.tsx | 26 +++++++++++++------ .../components/context-menu/context-menu.tsx | 3 +++ 8 files changed, 47 insertions(+), 20 deletions(-) diff --git a/.claude/rules/sim-list-ordering.md b/.claude/rules/sim-list-ordering.md index ad766adced5..7da33be5af8 100644 --- a/.claude/rules/sim-list-ordering.md +++ b/.claude/rules/sim-list-ordering.md @@ -26,12 +26,20 @@ Left-to-right becomes top-to-bottom. A toolbar reading `Filter · Sort · Export Platform-only entries (desktop **Browser** and **Terminal**) trail the shared set rather than interleaving, so the common prefix is identical on every platform. -## Grouping: one rule, before the destructive action +## Grouping: one rule, against the consequential group Order is governed above. **Separators are governed here** — and the answer is: use at most one. -Put a single `DropdownMenuSeparator` immediately before the destructive group (Delete, Leave, -Close, Hide) and nowhere else. Everything above it runs uninterrupted in toolbar-mirroring order. +Put a single `DropdownMenuSeparator` against the **consequential group** — the actions that +delete, detach, or change a run — and nowhere else. Everything on the other side of it runs +uninterrupted in toolbar-mirroring order. + +That group trails in almost every menu, so in practice the rule reads "one rule immediately +before Delete / Leave / Close / Hide". It leads in exactly one place: the **logs row menu**, +where `Retry` and `Cancel Run` are the primary actions on a failed run and sit at the top, with +the rule beneath them. Ordering follows the surface (see "The rule" above); the separator simply +fences whichever end the consequential group occupies. A menu whose consequential actions are +merely *disabled* still gets no extra rule — `disabled` is not a group. ```tsx // ✗ Bad — four semantic bands the user meets nowhere else diff --git a/CLAUDE.md b/CLAUDE.md index bc6b1ce36ae..7faf1862acc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -388,7 +388,7 @@ A list orders itself the way the user already reads the same things somewhere el Encode the order in ONE exported constant and sort by it — never a hand-maintained literal per menu (`RESOURCE_MENU_ORDER` / `byResourceMenuOrder` in `home/components/mothership-view/components/resource-registry`). Render mixed item kinds in a single ordered pass; emitting all submenu-backed families and then all flat ones silently pins every submenu to the top no matter what the constant says. Divergence is allowed only for search ranking, user-controlled ordering, and recency. -**Grouping**: at most ONE `DropdownMenuSeparator` per menu, immediately before the destructive group (Delete/Leave/Close/Hide). No toolbar in the app renders a divider, so multi-band menus teach a taxonomy that exists on no other surface. Build each separator's guard from the EXACT render conditions of the items on both sides — a looser guard is what leaves a dangling rule when its group is conditional. Never add a prop to move a rule. Full rule in `.claude/rules/sim-list-ordering.md`. +**Grouping**: at most ONE `DropdownMenuSeparator` per menu, fencing the consequential group — immediately before Delete/Leave/Close/Hide in almost every menu, and immediately after Retry/Cancel Run in the logs row menu, where those lead. No toolbar in the app renders a divider, so multi-band menus teach a taxonomy that exists on no other surface. Build each separator's guard from the EXACT render conditions of the items on both sides — a looser guard is what leaves a dangling rule when its group is conditional. Never add a prop to move a rule. Full rule in `.claude/rules/sim-list-ordering.md`. ## Styling diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx index 1fbe5068162..60b2b06d8c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx @@ -48,7 +48,7 @@ export const FilesListContextMenu = memo(function FilesListContextMenu({ {onUploadFile && ( - Upload file + Upload )} {onCreateFolder && ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index d03d8fa42d7..19a6b99f035 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -33,7 +33,7 @@ interface ChunkContextMenuProps { /** * Context menu for chunks table. - * Shows chunk actions when right-clicking a row, or "Create chunk" when right-clicking empty space. + * Shows chunk actions when right-clicking a row, or "New chunk" when right-clicking empty space. * Supports batch operations when multiple chunks are selected. */ export function ChunkContextMenu({ @@ -135,7 +135,7 @@ export function ChunkContextMenu({ onAddChunk && ( - Create chunk + New chunk ) )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index bb8fe70e9ca..4617eda16b8 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -141,7 +141,7 @@ export function DocumentContextMenu({ onAddDocument && ( - Add document + New documents ) )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index faa5cf0c112..12c1bd2d8bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -44,6 +44,12 @@ const LIMITED_RUN_PRESETS = [10, 1000] as const /** Labels for the table-scoped run items. With an active filter the run is * scoped to matching rows, so the labels say "filtered rows" to make the * narrowed target visible. Shared by both menu surfaces. */ +/** + * Incomplete before all, matching the action bar and the row context menu, which both + * present Play (empty or failed) ahead of Refresh (every row). These two menus read the + * same four run actions the user already met on the action bar, so they must not invert + * the pair — see `.claude/rules/sim-list-ordering.md`. + */ function runMenuLabels(hasActiveFilter: boolean) { const rows = hasActiveFilter ? 'filtered rows' : 'rows' return { @@ -171,12 +177,12 @@ export function ColumnOptionsMenu({ {`Run ${selectedRowCount} selected ${selectedRowCount === 1 ? 'row' : 'rows'}`} )} - onRunColumnAll?.()}> - {runLabels.all} - onRunColumnIncomplete?.()}> {runLabels.incomplete} + onRunColumnAll?.()}> + {runLabels.all} + {onRunColumnLimited && LIMITED_RUN_PRESETS.map((max) => ( onRunColumnLimited(max)}> @@ -510,10 +516,10 @@ export function WorkflowGroupMetaCell({ {`Run ${selectedCount} selected ${selectedCount === 1 ? 'row' : 'rows'}`} )} - {runLabels.all} {runLabels.incomplete} + {runLabels.all} {LIMITED_RUN_PRESETS.map((max) => ( handleRunLimited(max)}> {runLabels.limited(max)} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx index 0097cb1c3d9..a04fdb91445 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/table-context-menu/table-context-menu.tsx @@ -57,6 +57,23 @@ export function TableContextMenu({ disableImport = false, disableExport = false, }: TableContextMenuProps) { + /** + * `Move to` needs a NON-EMPTY `moveOptions`, not just the handler — the looser + * `onMove` alone draws the rule with nothing above it for a table whose other + * actions are all absent and whose move list is empty. + * + * @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive + * group, with both sides built from the items' exact render conditions. + */ + const hasActionsAboveDestructive = + onViewSchema || + onRename || + onImportCsv || + onExportCsv || + (onMove && moveOptions && moveOptions.length > 0) || + onCopyId || + onTogglePin + return ( !open && onClose()} modal={false}> @@ -126,14 +143,7 @@ export function TableContextMenu({ Copy ID )} - {(onViewSchema || - onRename || - onImportCsv || - onExportCsv || - onMove || - onCopyId || - onTogglePin) && - onDelete && } + {hasActionsAboveDestructive && onDelete && } {onDelete && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index aa623fa9d84..3e343289aac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -185,6 +185,9 @@ export function ContextMenu({ * rendered a trailing rule under the last item. */ const hasActionsAboveDestructive = + /* No `openInNewTabPosition` term: the item renders in the 'first' slot or the + 'last' one, and the prop is a closed two-value union, so `showOpenInNewTab && + onOpenInNewTab` already means exactly "the nav item renders somewhere above". */ (showOpenInNewTab && onOpenInNewTab) || (showMarkAsRead && onMarkAsRead) || (showMarkAsUnread && onMarkAsUnread) || From 349c42a3b6738ed69f47a41d76c30a91a722c286 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 21 Aug 2026 21:14:57 -0700 Subject: [PATCH 3/4] fix(menus): apply the one-rule grouping to the folder context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The folder row menu kept a separator at its canEdit permission boundary plus one before Delete — the same shape already corrected in the file row menu, missed because the sweep that found it did not cover this file. Open and Pin above are unconditional, so the surviving rule is always backed on both sides. Also records the standing exception the sweep surfaced: the text editor, terminal, and browser page menus emulate native OS menus, whose banding the user learns outside Sim. That is the ordering rule's own principle — mirror the surface they already read — so those keep their banding while our own resource and row menus, whose toolbars are flat, take one rule. --- .claude/rules/sim-list-ordering.md | 9 +++++++++ .../components/folders/folder-context-menu.tsx | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.claude/rules/sim-list-ordering.md b/.claude/rules/sim-list-ordering.md index 7da33be5af8..5f2bbee86e9 100644 --- a/.claude/rules/sim-list-ordering.md +++ b/.claude/rules/sim-list-ordering.md @@ -59,6 +59,15 @@ genuinely buys is a stop before the action you cannot undo. A second rule is justified only when a menu mixes genuinely different *scopes* — cell-level and table-level actions in one menu, say — not different verbs. +**The one standing exception: menus that emulate a native menu.** The text-editor menu +(`editor-context-menu.tsx`), the terminal menu (`terminal-context-menu.tsx`), and the browser +page menu (`browser-session.tsx`) each mirror the OS menu the user already knows — clipboard +banding (`Cut · Copy · Paste │ Select all`) is a convention every text field on their machine +teaches them. These keep their native banding, and that is the *same* principle as the ordering +rule above: mirror the surface the user already reads. The test is whether a real menu outside +Sim taught them the grouping. Our own resource, row, and action menus have no such precedent — +the toolbars they mirror are flat — so they take the single rule. + **Both sides of every rule must be guaranteed non-empty.** Write the separator's guard out of the *exact* render conditions of the items around it, never a looser approximation: diff --git a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx index 41b8682aef6..62d0ec5cacf 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/folders/folder-context-menu.tsx @@ -91,7 +91,6 @@ export const FolderContextMenu = memo(function FolderContextMenu({ )} {canEdit && ( <> - Rename From aa8b8770b13f3fc27353c33ccfd0a887755c6e64 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 22 Aug 2026 10:04:31 -0700 Subject: [PATCH 4/4] fix(menus): keep empty-row actions together --- .../chunk-context-menu/chunk-context-menu.tsx | 1 - .../document-context-menu.tsx | 1 - .../knowledge-base-context-menu.tsx | 1 - .../headers/workflow-group-meta-cell.tsx | 8 +++---- .../context-menu/context-menu.test.tsx | 10 --------- .../components/context-menu/context-menu.tsx | 22 ------------------- 6 files changed, 4 insertions(+), 39 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index 19a6b99f035..8a73d105395 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -71,7 +71,6 @@ export function ChunkContextMenu({ const hasEditSection = !isMultiSelect && (!!onEdit || !!onCopyContent) const hasStateSection = !!onToggleEnabled const hasDestructiveSection = !!onDelete - /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ const hasActionsAboveDestructive = hasNavigationSection || hasEditSection || hasStateSection return ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index 4617eda16b8..3f3cb03f63c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -71,7 +71,6 @@ export function DocumentContextMenu({ const hasEditSection = !isMultiSelect && (!!onRename || !!onViewTags) const hasStateSection = !!onToggleEnabled const hasDestructiveSection = !!onDelete - /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ const hasActionsAboveDestructive = hasNavigationSection || hasEditSection || hasStateSection return ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx index f815043a66c..2f5fdad09e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-base-context-menu/knowledge-base-context-menu.tsx @@ -75,7 +75,6 @@ export const KnowledgeBaseContextMenu = memo(function KnowledgeBaseContextMenu({ const hasMoveSection = !disableEdit && !!onMove && !!moveOptions && moveOptions.length > 0 const hasEditSection = (showEdit && !!onEdit) || hasMoveSection const hasDestructiveSection = showDelete && !!onDelete - /** @see `.claude/rules/sim-list-ordering.md` — one rule, before the destructive group. */ const hasActionsAboveDestructive = hasNavigationSection || hasInfoSection || hasEditSection return ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 12c1bd2d8bd..2e1f34ee226 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -180,15 +180,15 @@ export function ColumnOptionsMenu({ onRunColumnIncomplete?.()}> {runLabels.incomplete} - onRunColumnAll?.()}> - {runLabels.all} - {onRunColumnLimited && LIMITED_RUN_PRESETS.map((max) => ( onRunColumnLimited(max)}> {runLabels.limited(max)} ))} + onRunColumnAll?.()}> + {runLabels.all} + )} @@ -519,12 +519,12 @@ export function WorkflowGroupMetaCell({ {runLabels.incomplete} - {runLabels.all} {LIMITED_RUN_PRESETS.map((max) => ( handleRunLimited(max)}> {runLabels.limited(max)} ))} + {runLabels.all} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx index f7dd102d4c3..12b7d7c8902 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.test.tsx @@ -138,15 +138,6 @@ describe('sidebar context menu dismissal', () => { }) }) -/** - * Separator invariants. The menu carries exactly one rule, immediately before the - * destructive group, and it may never render with an empty group on either side — - * see the grouping section of `.claude/rules/sim-list-ordering.md`. - * - * These pin the shape the flag matrix used to get wrong: `showLeave` in the rule's - * guard without the `&& onLeave` its item requires produced a trailing rule under - * the last item, and nothing covered it. - */ describe('separators', () => { function renderWith(props: Partial>) { ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true @@ -169,7 +160,6 @@ describe('separators', () => { ) } - /** Menu children in render order, each as 'sep' or its label. */ function menuShape(): string[] { const content = document.querySelector('[role="menu"]') if (!content) return [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 3e343289aac..3d4cfef1a2e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -165,29 +165,7 @@ export function ContextMenu({ showUploadLogo = false, disableUploadLogo = false, }: ContextMenuProps) { - /** - * One rule, immediately before the destructive group — see the menu-grouping - * section of `.claude/rules/sim-list-ordering.md`. - * - * This menu previously carried four semantic bands (navigation / status / edit / - * copy / destructive) behind up to five separators. No toolbar in the app renders - * a divider — every header is a flat `gap-1` chip row — so those bands taught a - * taxonomy the user met nowhere else, and each caller's flag combination banded - * the same action differently (Pin alone here, Pin beside Duplicate there). Order - * still mirrors the surface's toolbar, which is what the ordering rule actually - * requires; only the rules between groups are gone. - * - * Every term below is the exact render condition of the item it stands for, so a - * separator can never outlive the group on either side of it. `showLeave` was the - * one asymmetric term — it omitted `&& onLeave`, so a caller passing `showLeave` - * from a permission check with a conditional `onLeave` (the `x ? fn : undefined` - * shape used for `onDuplicate`/`onTogglePin`/`onCloseTab` elsewhere) would have - * rendered a trailing rule under the last item. - */ const hasActionsAboveDestructive = - /* No `openInNewTabPosition` term: the item renders in the 'first' slot or the - 'last' one, and the prop is a closed two-value union, so `showOpenInNewTab && - onOpenInNewTab` already means exactly "the nav item renders somewhere above". */ (showOpenInNewTab && onOpenInNewTab) || (showMarkAsRead && onMarkAsRead) || (showMarkAsUnread && onMarkAsUnread) ||