Skip to content

Commit 9277e7d

Browse files
j15zclaude
andauthored
feat(search): page-aware cmd+k palette with ask-Sim mode (#6518)
* fix(chat): stop losing sends aborted during mount-settling * fix(chat): detect aborts by signal state, not error identity fetch rejects with the RAW abort reason when its signal carries one — abort('unmount:client_cleanup') surfaces as a plain string, so every err.name === 'AbortError' check missed it and the restore path never ran (verified live). The test stub now rejects with the raw reason like real fetch, which turns this gap red. * fix(chat): hand an aborted chatless send to the next mount The mount-settling cycle is a full remount — the pending chat key is regenerated per instance, so restoring the aborted send into the dead instance's queue orphaned it (verified live). A chatless send now re-persists as a one-shot MothershipHandoffStorage handoff the next mount's consumer re-sends; chat-bound sends keep the queue restore. * fix(chat): deliver an aborted chatless send to the live replacement surface The settling remount's consumer checks handoff storage before the restore microtask re-persists it, so the stored handoff sat unread until a navigation. The replacement surface's send listener IS registered by restore time — deliver the message directly through the claimable send event, keeping the stored handoff as the no-surface fallback. * refactor(chat): thread the recoverable-abort outcome through the send result Replaces the restorableCleanupAbortRef reset choreography with a widened startSendMessage return ('recoverable_cleanup_abort'), so the restore decision is ordinary data flow and the second caller cannot leave a stale flag behind. * fix(chat): carry attachments through the cross-mount send handoff The recoverable-abort delivery excluded attachment-bearing sends, so they restored under the dead instance's pending key and were silently lost. The claimable send event now carries fileAttachments end to end (dispatcher, home listener, restore path); only the storage fallback — whose shape cannot hold attachments — still queue-restores them. * fix(panel): forward event attachments to the copilot send * fix(chat): carry attachments through the stored handoff lane too The unclaimed-event fallback excluded attachment sends and restored them under the disposed mount's pending key. The persisted handoff now carries fileAttachments (they are plain references to already-uploaded files), the home consumer forwards them, and the recovery branch always hands off — no stranded lane remains. * feat(search): improve command palette results * feat(search): sharpen command palette discovery Unify command surfaces, flatten ranked results, and remove favorites so the palette stays focused on fast discovery. Add Tab result cycling and workspace identity icons for quicker keyboard navigation. * feat(search): unify cmd+k into one page-aware section model Every palette view now derives from a single rule: the page's action group, its own entity section hoisted, Platform actions, then a fixed tail shared by all pages. Adds page commands for table/file/KB details, logs, and deploy; a Logs section with run dates; chat last-activity receipts; kebab-cased secondary search text with per-entry scattered matching; exact-section-name ranking lifts; and scroll/selection fixes on open, loop, and arrow navigation. Removes the canvas block/tool/trigger/docs sections and the store's unused section restriction and pending-connect plumbing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(search): ask-Sim tab mode, canvas sections, and palette refinements Tab now flips the palette into ask mode — Enter lands on Chat with the query seeded via the proven curated-prompt handoff (auto-send deferred on a diagnosed use-chat mount-abort bug) — replacing the no-results New Chat fallback. Restores the canvas Blocks/Triggers/Tools/Tool operations sections between the workflow Actions group and Sim, keeps the integrations catalog and connected accounts off the canvas, renames the global group to Sim and page groups to Actions, puts an exactly-named page above its lifted contents, adds chat last-activity receipts, and softens the list chrome (hidden scrollbar, shorter fade, scroll-margin fixes for arrow and loop navigation). Also renames the generic webhook block to Webhook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(search): order module results below Sim actions * fix(review): make command palette fast and focused * fix(search): restore Ask Sim prefill and refine palette ranking * chore(sidebar): drop unused getSettingsHref destructure * fix(deploy): gate every deploy invoker on full eligibility * fix(search): port the palette to post-#6458 staging Carries the former merge resolutions as one commit: the emcn icon set (SelectAll fit-to-view, Search chrome), native browser-panel occlusion gating, scheduled-tasks retirement, the chip-aware handoff consumer superseding the ?handoff=1 machinery, and Knowledge bases pluralization. * feat(search): auto-send Ask Sim queries via the chat handoff * revert(search): return Ask Sim to prefill, storing raw prose Auto-send still loses the message on cross-route navigation — use-chat's cleanup abort fires during Home's mount-settling effect cycle. Prefill restored, but via LandingPromptStorage directly so free-form queries are never mentionified into @ chips. * feat(search): auto-send Ask Sim queries via the chat handoff Re-lands the auto-send flip: with fix/mship-mount-send-loss beneath this branch, sends started during Home's mount-settling window survive the cleanup abort (queued, restored, re-dispatched), so the handoff no longer loses the query on cross-route navigation. * fix(deploy): include registry loading in the deploy invoker gate * improvement(search): label the ask row New Chat * refactor(search): apply simplify-pass cleanups - rank against a deferred query and hoist search-independent derivations so typing never blocks on the cross-section re-rank - cache secondary-text tokenization (hottest per-keystroke loop) - skip building browse groups mid-search; gate the palette-only credentials and logs queries on the palette being open - flatten getGlobalSearchResults onto a spec-stable sort - drop the dead store-side SEARCH_SECTIONS/docs path, the no-op CommandSearch surface variant, a duplicate hex regex, a dead font-base class, and the TaskItem/FolderedItem shape overlap * fix(search): paint the palette fog with the dialog's own surface The frost under the floating input reused the canvas card's --surface-2 gradient, which reads as a tinted band on the palette's --surface-4/ --surface-5 dialog (visible in dark mode). The CommandSearch surface variant returns — this time with genuinely different values — and the chrome test pins the host-matching fog. * fix(search): the palette fog matches the inner --bg panel, not the dialog ring * fix(search): review-round parity and consistency fixes - table import command respects the in-progress upload gate - Export CSV is offered to viewers (matching the header control) - palette mode flips with the deferred query the ranking ran against - a gated palette deploy reports the button tooltip's reason via toast * fix(deploy): use the emcn toast input shape * fix(search): rename logs view toggles to "Switch to Logs/Dashboard" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2f43148 commit 9277e7d

28 files changed

Lines changed: 3212 additions & 859 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
isUntitledName,
104104
uniqueMarkdownName,
105105
} from '@/app/workspace/[workspaceId]/files/untitled-title'
106+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
106107
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
107108
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
108109
import { usePinItem, usePinnedIds, useUnpinItem } from '@/hooks/queries/pinned-items'
@@ -1681,6 +1682,16 @@ export function Files() {
16811682
fileInputRef.current?.click()
16821683
}, [canEdit, uploading])
16831684

1685+
useRegisterGlobalCommands(() => [
1686+
{ id: 'files-upload', handler: () => handleUploadClick() },
1687+
{ id: 'files-new-file', handler: () => void handleCreateFile() },
1688+
{ id: 'files-new-folder', handler: () => void handleCreateFolder() },
1689+
{ id: 'file-download', handler: () => handleDownloadSelected() },
1690+
{ id: 'file-rename', handler: () => handleStartHeaderRename() },
1691+
{ id: 'file-share', handler: () => handleShareSelected() },
1692+
{ id: 'file-delete', handler: () => handleDeleteSelected() },
1693+
])
1694+
16841695
const searchConfig: SearchConfig = {
16851696
value: urlSearchTerm,
16861697
onChange: setSearchTerm,

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {
7777
pageUrlKeys,
7878
} from '@/app/workspace/[workspaceId]/knowledge/[id]/search-params'
7979
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
80+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
8081
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
8182
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
8283
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
@@ -707,6 +708,14 @@ export function KnowledgeBase({
707708
setShowAddDocumentsModal(true)
708709
}
709710

711+
useRegisterGlobalCommands(() => [
712+
{ id: 'knowledge-base-new-documents', handler: () => setShowAddDocumentsModal(true) },
713+
{ id: 'knowledge-base-new-connector', handler: () => setShowAddConnectorModal(true) },
714+
{ id: 'knowledge-base-rename', handler: () => kbRename.startRename(id, knowledgeBaseName) },
715+
{ id: 'knowledge-base-tags', handler: () => setShowTagsModal(true) },
716+
{ id: 'knowledge-base-delete', handler: () => setShowDeleteDialog(true) },
717+
])
718+
710719
/**
711720
* Handles bulk enabling of selected documents
712721
*/

apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
knowledgeUrlKeys,
6060
} from '@/app/workspace/[workspaceId]/knowledge/search-params'
6161
import { filterKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/utils/filter'
62+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
6263
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
6364
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
6465
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
@@ -726,6 +727,11 @@ export function Knowledge() {
726727
// eslint-disable-next-line react-hooks/exhaustive-deps
727728
}, [workspaceId])
728729

730+
useRegisterGlobalCommands(() => [
731+
{ id: 'knowledge-new-base', handler: () => handleOpenCreateModal() },
732+
{ id: 'knowledge-new-folder', handler: () => void handleCreateFolder() },
733+
])
734+
729735
const handleRenameFolder = useCallback(() => {
730736
const folder = activeFolderRef.current
731737
if (!folder) return

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
logSortParams,
7070
} from '@/app/workspace/[workspaceId]/logs/search-params'
7171
import type { Suggestion } from '@/app/workspace/[workspaceId]/logs/types'
72+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
7273
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
7374
import { getBlock } from '@/blocks/registry'
7475
import { useFolderMap, useFolders } from '@/hooks/queries/folders'
@@ -698,6 +699,13 @@ export default function Logs() {
698699
debouncedSearchQuery,
699700
])
700701

702+
useRegisterGlobalCommands(() => [
703+
{ id: 'logs-refresh', handler: () => handleRefresh() },
704+
{ id: 'logs-export', handler: () => void handleExport() },
705+
{ id: 'logs-show-dashboard', handler: () => setViewMode('dashboard') },
706+
{ id: 'logs-show-logs', handler: () => setViewMode('logs') },
707+
])
708+
701709
const loadMoreLogs = useCallback(() => {
702710
const { isFetching, hasNextPage, fetchNextPage } = logsQueryRef.current
703711
if (!isFetching && hasNextPage) {

apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ export interface ParsedShortcut {
2727

2828
export interface GlobalCommand {
2929
id?: string
30-
shortcut: string
30+
/** Keyboard binding. Omit for palette-only commands invoked by id. */
31+
shortcut?: string
3132
allowInEditable?: boolean
3233
handler: (event: KeyboardEvent) => void
3334
}
3435

3536
interface RegistryCommand extends GlobalCommand {
3637
id: string
37-
parsed: ParsedShortcut
38+
parsed: ParsedShortcut | null
3839
}
3940

4041
interface GlobalCommandsContextValue {
@@ -130,7 +131,7 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
130131
const createdIds: string[] = []
131132
for (const cmd of commands) {
132133
const id = cmd.id ?? generateId()
133-
const parsed = parseShortcut(cmd.shortcut)
134+
const parsed = cmd.shortcut ? parseShortcut(cmd.shortcut) : null
134135
registryRef.current.set(id, {
135136
...cmd,
136137
id,
@@ -152,6 +153,7 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
152153
if (e.isComposing) return
153154

154155
for (const [, cmd] of registryRef.current) {
156+
if (!cmd.parsed) continue
155157
if (!cmd.allowInEditable && isEditableElement(document.activeElement)) continue
156158

157159
if (matchesShortcut(e, cmd.parsed)) {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
} from '@/app/workspace/[workspaceId]/components/folders'
3737
import { PresenceAvatars } from '@/app/workspace/[workspaceId]/components/presence/presence-avatars'
3838
import { LogDetails } from '@/app/workspace/[workspaceId]/logs/components'
39+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
3940
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
4041
import { ImportCsvDialog } from '@/app/workspace/[workspaceId]/tables/components/import-csv-dialog'
4142
import { ImportProgressMenu } from '@/app/workspace/[workspaceId]/tables/components/import-progress-menu'
@@ -1068,6 +1069,34 @@ export function Table({
10681069
}
10691070
}, [tableData, workspaceId])
10701071

1072+
useRegisterGlobalCommands(() => [
1073+
{
1074+
id: 'table-new-column',
1075+
handler: () => {
1076+
if (!userPermissions.canEdit) return
1077+
if (tableDataRef.current?.locks.schemaLocked) {
1078+
showBlockedToast('add-column')
1079+
return
1080+
}
1081+
handleAddColumnOfType('string')
1082+
},
1083+
},
1084+
{
1085+
id: 'table-export-csv',
1086+
handler: () => {
1087+
if (!tableDataRef.current?.rowCount) return
1088+
void handleExportCsv()
1089+
},
1090+
},
1091+
{
1092+
id: 'table-import-csv',
1093+
handler: () => {
1094+
if (!userPermissions.canEdit || tableDataRef.current?.locks.insertLocked) return
1095+
onRequestImportCsv()
1096+
},
1097+
},
1098+
])
1099+
10711100
const columnOptions = useMemo<ColumnOption[]>(
10721101
() =>
10731102
columns.map((col) => ({

apps/sim/app/workspace/[workspaceId]/tables/tables.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
useFolderNavigation,
4747
useFolderRowDragDrop,
4848
} from '@/app/workspace/[workspaceId]/components/folders'
49+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
4950
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
5051
import {
5152
ImportCsvDialog,
@@ -1033,6 +1034,17 @@ export function Tables() {
10331034
}
10341035
}, [workspaceId, folders, currentFolderId, createFolderAsync, setSearchTerm, startFolderRename])
10351036

1037+
useRegisterGlobalCommands(() => [
1038+
{ id: 'tables-new-table', handler: () => void handleCreateTable() },
1039+
{ id: 'tables-new-folder', handler: () => void handleCreateFolder() },
1040+
{
1041+
id: 'tables-import-csv',
1042+
handler: () => {
1043+
if (!uploading) csvInputRef.current?.click()
1044+
},
1045+
},
1046+
])
1047+
10361048
const headerActions: ResourceAction[] = useMemo(
10371049
() => [
10381050
{

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
44
import { Button, cn } from '@sim/emcn'
5-
import { Search, X } from '@sim/emcn/icons'
5+
import { X } from '@sim/emcn/icons'
66
import { WorkflowBlockBorder, type WorkflowBorderPort } from '@sim/workflow-renderer'
77
import { Command } from 'cmdk'
88
import { useParams } from 'next/navigation'
99
import { usePostHog } from 'posthog-js/react'
1010
import { Handle, type NodeProps, Position } from 'reactflow'
1111
import { captureEvent } from '@/lib/posthog/client'
12+
import {
13+
CommandFadedList,
14+
CommandSearch,
15+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome'
1216
import { MemoizedCommandItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items'
1317
import {
1418
BlocksGroup,
@@ -403,10 +407,11 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS
403407
className='relative z-20 flex h-full flex-col overflow-hidden rounded-2xl [clip-path:inset(0_round_16px)]'
404408
>
405409
<div className='relative min-h-0 flex-1'>
406-
<Command.List
410+
<CommandFadedList
407411
ref={listRef}
412+
fade='canvas'
408413
className={cn(
409-
"nodrag nopan nowheel allow-scroll scrollbar-none [&_[cmdk-item][aria-selected='true']]:!border-transparent [&_[cmdk-item][aria-selected='true']]:!bg-[var(--surface-hover)] [&_[cmdk-item]_svg]:!scale-100 [&_[cmdk-item]_svg]:!transition-none h-full overflow-y-auto overflow-x-hidden px-1.5 pt-12 pb-1.5 [-webkit-mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_18%,black_94%,transparent_100%)] [clip-path:inset(3px_round_13px)] [mask-image:linear-gradient(to_bottom,transparent_0%,transparent_8%,black_18%,black_94%,transparent_100%)] [&_[cmdk-group-items]]:flex [&_[cmdk-group-items]]:flex-col",
414+
"nodrag nopan nowheel allow-scroll scrollbar-none [&_[cmdk-item][aria-selected='true']]:!border-transparent [&_[cmdk-item][aria-selected='true']]:!bg-[var(--surface-hover)] [&_[cmdk-item]_svg]:!scale-100 [&_[cmdk-item]_svg]:!transition-none h-full [clip-path:inset(3px_round_13px)]",
410415
CMDK_ITEM_GAP_CLASS,
411416
CMDK_SECTION_GAP_CLASS
412417
)}
@@ -482,19 +487,16 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS
482487
<ToolsGroup items={browseTools} onSelect={handleToolSelect} />
483488
</>
484489
)}
485-
</Command.List>
486-
<div className='nodrag nopan absolute inset-x-[3px] top-[3px] z-20 flex h-12 cursor-text items-center gap-2 rounded-t-[13px] bg-[linear-gradient(to_bottom,var(--surface-2)_0%,color-mix(in_srgb,var(--surface-2)_88%,transparent)_68%,transparent_100%)] px-2.5 pb-2'>
487-
<Search className='size-[14px] flex-shrink-0 text-[var(--text-muted)]' />
488-
<Command.Input
489-
ref={inputRef}
490-
autoFocus
491-
aria-label='Search blocks'
492-
value={search}
493-
onValueChange={handleSearchChange}
494-
placeholder='Search blocks...'
495-
className='h-8 min-w-0 flex-1 cursor-text bg-transparent text-[var(--text-body)] text-sm outline-none placeholder:text-[var(--text-muted)] focus:outline-none'
496-
/>
497-
</div>
490+
</CommandFadedList>
491+
<CommandSearch
492+
ref={inputRef}
493+
surface='canvas'
494+
autoFocus
495+
aria-label='Search blocks'
496+
value={search}
497+
onValueChange={handleSearchChange}
498+
placeholder='Search blocks...'
499+
/>
498500
</div>
499501
</Command>
500502
</div>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Chip, Tooltip } from '@sim/emcn'
4+
import { Chip, Tooltip, toast } from '@sim/emcn'
5+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
56
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
67
import {
78
useChangeDetection,
@@ -62,7 +63,7 @@ export function Deploy({ activeWorkflowId, userPermissions, disabled = false }:
6263
(!isDeployed && deployReadiness.isBlocked && !deployReadiness.isSyncing)
6364

6465
const onDeployClick = async () => {
65-
if (disabled || !canDeploy || !activeWorkflowId) return
66+
if (isRegistryLoading || isDisabled || !activeWorkflowId) return
6667

6768
if (isDeploymentSettling) {
6869
setIsModalOpen(true)
@@ -75,6 +76,21 @@ export function Deploy({ activeWorkflowId, userPermissions, disabled = false }:
7576
}
7677
}
7778

79+
useRegisterGlobalCommands(() => [
80+
{
81+
id: 'deploy-workflow',
82+
handler: () => {
83+
/* The palette can't render a disabled state for this action yet, so a
84+
gated invocation reports the same reason the button's tooltip shows. */
85+
if (isRegistryLoading || isDisabled) {
86+
toast({ message: isRegistryLoading ? 'Workflow is still loading' : getTooltipText() })
87+
return
88+
}
89+
void onDeployClick()
90+
},
91+
},
92+
])
93+
7894
const getTooltipText = () => {
7995
if (isEmpty) {
8096
return 'Cannot deploy an empty workflow'

0 commit comments

Comments
 (0)