import { useEnvironmentScope } from "../../state/session"; import { useEnvironmentsWithScope, readEnvironmentScope } from "../../state/session"; import { ChevronDownIcon, CircleXIcon, EllipsisIcon, FileJsonIcon, MinusIcon, PlusIcon, SearchIcon, TriangleAlertIcon, XIcon, } from "lucide-react"; import { useLocation } from "@tanstack/react-router"; import { type KeyboardEvent, type ReactNode, type RefObject, useReducer, useRef, useState, } from "react"; import { flushSync } from "react-dom"; import { AuthOrchestrationOperateScope, AuthSettingsWriteScope, type KeybindingCommand, type KeybindingWhenNode, type ServerRemoveKeybindingInput, type ServerUpsertKeybindingInput, } from "@t3tools/contracts"; import { mergeWithDefaultKeybindings } from "@t3tools/shared/keybindings"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { isElectron } from "../../env"; import { useOpenInPreferredEditor } from "../../editorPreferences"; import { formatShortcutLabel } from "../../keybindings"; import { cn } from "../../lib/utils"; import { serverEnvironment } from "../../state/server"; import { useSettingsScope } from "./SettingsScopeContext"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; import { Kbd, KbdGroup } from "../ui/kbd"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { Toggle } from "../ui/toggle"; import { toastManager } from "../ui/toast"; import { buildKeybindingRows, filterKeybindingRows, buildKeybindingCommandOptions, buildWhenVariableOptions, commandLabel, DEFAULT_WHEN_VARIABLE, isKnownWhenVariable, keybindingConflictLabels, commandRecordsEscape, keybindingFromKeyboardEvent, parseWhenExpressionDraft, groupKeybindingRows, type KeybindingCommandOption, type KeybindingGroup, type KeybindingRow, type WhenVariableOption, unknownWhenVariables, whenAstToExpression, whenNodeRemoveLabel, } from "./KeybindingsSettings.logic"; import { SettingsGroup } from "./SettingsGroup"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; import { keybindingSearchAnchorId, searchableSetting } from "./settingsSearch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { useAtomCommand } from "../../state/use-atom-command"; function KeybindingPill({ value }: { value: string }) { // Keys dedupe repeated parts; a literal "+" in a shortcut splits into empty strings. const seenParts = new Map(); const parts = value.split("+").map((part) => { const seen = seenParts.get(part) ?? 0; seenParts.set(part, seen + 1); return { part, key: seen === 0 ? part : `${part}-${seen}` }; }); return ( {parts.map(({ part, key }) => ( {part === "mod" ? navigator.platform.toLowerCase().includes("mac") ? "⌘" : "Ctrl" : part === "shift" ? "⇧" : part === "alt" ? navigator.platform.toLowerCase().includes("mac") ? "⌥" : "Alt" : part === "ctrl" ? "⌃" : part.length === 1 ? part.toUpperCase() : part} ))} ); } function KeybindingsSearchInput({ query, onChange, inputRef, }: { query: string; onChange: (next: string) => void; inputRef: RefObject; }) { return ( onChange(event.currentTarget.value)} onKeyDown={(event) => { if (event.key !== "Escape") return; event.preventDefault(); if (query.length > 0) onChange(""); else event.currentTarget.blur(); }} placeholder="Search keybindings" aria-label="Search keybindings" size="sm" /> ); } type BooleanOperator = "and" | "or"; function flattenWhenChildren( node: KeybindingWhenNode, operator: BooleanOperator, ): KeybindingWhenNode[] { if (node.type !== operator) return [node]; return [ ...flattenWhenChildren(node.left, operator), ...flattenWhenChildren(node.right, operator), ]; } function buildWhenExpressionGroup( children: readonly KeybindingWhenNode[], operator: BooleanOperator, ): KeybindingWhenNode | undefined { const first = children[0]; if (!first) return undefined; return children.slice(1).reduce( (left, right) => ({ type: operator, left, right, }), first, ); } function conditionParts(node: KeybindingWhenNode): { identifier: string; negated: boolean } | null { if (node.type === "identifier") return { identifier: node.name, negated: false }; if (node.type === "not" && node.node.type === "identifier") { return { identifier: node.node.name, negated: true }; } return null; } function setConditionIdentifier(node: KeybindingWhenNode, identifier: string): KeybindingWhenNode { const parts = conditionParts(node); if (!parts) return node; const next: KeybindingWhenNode = { type: "identifier", name: identifier }; return parts.negated ? { type: "not", node: next } : next; } function setConditionNegated(node: KeybindingWhenNode, negated: boolean): KeybindingWhenNode { const parts = conditionParts(node); if (!parts) return negated ? { type: "not", node } : node; const identifier: KeybindingWhenNode = { type: "identifier", name: parts.identifier }; return negated ? { type: "not", node: identifier } : identifier; } function defaultWhenCondition(): KeybindingWhenNode { return { type: "identifier", name: DEFAULT_WHEN_VARIABLE }; } function defaultWhenGroup(operator: BooleanOperator = "and"): KeybindingWhenNode { return { type: operator, left: defaultWhenCondition(), right: { type: "not", node: defaultWhenCondition() }, }; } function WarningTooltipIcon({ label, focusable = true, className, children, }: { label: string; focusable?: boolean; className?: string | undefined; children: ReactNode; }) { return ( } > {children} ); } function UnknownWhenVariableWarning({ identifiers, focusable = true, }: { identifiers: ReadonlyArray; focusable?: boolean; }) { if (identifiers.length === 0) return null; const label = identifiers.length === 1 ? `Unknown condition: ${identifiers[0]}` : `Unknown conditions: ${identifiers.join(", ")}`; return ( T3 Code does not recognize this condition yet. It can still be saved, but it may not match unless the runtime provides it. ); } function KeybindingConflictWarning({ labels }: { labels: ReadonlyArray }) { if (labels.length === 0) return null; const description = labels.length === 1 ? `Conflicts with ${labels[0]}.` : `Conflicts with ${labels.slice(0, 3).join(", ")}${labels.length > 3 ? ", and more" : ""}.`; return ( {description} The most recent matching binding wins when both conditions can apply. ); } function WhenVariableSelect({ value, variables, unknownIdentifiers, onChange, }: { value: string; variables: ReadonlyArray; unknownIdentifiers?: ReadonlyArray; onChange: (value: string) => void; }) { const selected = variables.find((option) => option === value); const options = selected || variables.some((option) => option === value) ? variables : [value, ...variables]; return ( ); } function WhenExpressionRemoveButton({ label, className, onRemove, }: { label: string; className?: string | undefined; onRemove: () => void; }) { return ( } > {label} ); } function WhenExpressionNodeEditor({ node, variables, depth = 0, onChange, onRemove, }: { node: KeybindingWhenNode; variables: ReadonlyArray; depth?: number; onChange: (node: KeybindingWhenNode) => void; onRemove?: () => void; }) { const condition = conditionParts(node); if (condition) { const unknownIdentifiers = isKnownWhenVariable(condition.identifier) ? [] : [condition.identifier]; return (
onChange(setConditionNegated(node, pressed))} aria-label={`Negate ${condition.identifier}`} variant="outline" size="compact" className="min-w-10" > Not onChange(setConditionIdentifier(node, value))} /> {onRemove ? ( ) : null}
); } if (node.type === "not") { return (
0 && "border-border/50 bg-background/50", )} >
onChange(pressed ? node : node.node)} aria-label="Negate group" variant="outline" size="compact" className="min-w-10" > Not {onRemove ? ( ) : null}
onChange({ type: "not", node: next })} />
); } const operator: BooleanOperator = node.type === "or" ? "or" : "and"; const children = flattenWhenChildren(node, operator); const childKeyCounts = new Map(); const childEntries = children.map((child) => { const baseKey = `${child.type}-${whenAstToExpression(child)}`; const count = childKeyCounts.get(baseKey) ?? 0; childKeyCounts.set(baseKey, count + 1); return { child, key: count === 0 ? baseKey : `${baseKey}-${count}` }; }); const updateChild = (target: KeybindingWhenNode, next: KeybindingWhenNode) => { let didUpdate = false; const nextChildren = children.map((child) => { if (!didUpdate && child === target) { didUpdate = true; return next; } return child; }); const nextNode = buildWhenExpressionGroup(nextChildren, operator); if (nextNode) onChange(nextNode); }; const removeChild = (target: KeybindingWhenNode) => { let didRemove = false; const nextChildren = children.filter((child) => { if (!didRemove && child === target) { didRemove = true; return false; } return true; }); const nextNode = buildWhenExpressionGroup(nextChildren, operator); if (nextNode) { onChange(nextNode); } else { onChange(defaultWhenCondition()); } }; const setOperator = (nextOperator: BooleanOperator) => { if (nextOperator === operator) return; const nextNode = buildWhenExpressionGroup(children, nextOperator); if (nextNode) onChange(nextNode); }; const addCondition = () => { const nextNode = buildWhenExpressionGroup([...children, defaultWhenCondition()], operator); if (nextNode) onChange(nextNode); }; const addGroup = () => { const nestedOperator: BooleanOperator = operator === "and" ? "or" : "and"; const group: KeybindingWhenNode = { type: nestedOperator, left: defaultWhenCondition(), right: { type: "not", node: defaultWhenCondition() }, }; const nextNode = buildWhenExpressionGroup([...children, group], operator); if (nextNode) onChange(nextNode); }; return (
0 && "border-border/70 bg-background/55", )} >
{onRemove ? ( ) : null}
{childEntries.map(({ child, key }) => (
updateChild(child, next)} onRemove={() => removeChild(child)} />
))}
); } function WhenExpressionBuilder({ value, variables, onChange, onValidityChange, }: { value: KeybindingWhenNode | undefined; variables: ReadonlyArray; onChange: (value: KeybindingWhenNode | undefined) => void; onValidityChange?: (valid: boolean) => void; }) { const expression = whenAstToExpression(value); const [expressionDraft, setExpressionDraft] = useState(expression); const parseResult = parseWhenExpressionDraft(expressionDraft); const parseError = parseResult.ok ? null : parseResult.message; const unknownIdentifiers = parseResult.ok ? unknownWhenVariables(parseResult.value) : []; const updateExpressionDraft = (nextExpression: string) => { setExpressionDraft(nextExpression); const nextResult = parseWhenExpressionDraft(nextExpression); onValidityChange?.(nextResult.ok); if (nextResult.ok) { onChange(nextResult.value); } }; const updateExpressionValue = (nextValue: KeybindingWhenNode | undefined) => { setExpressionDraft(whenAstToExpression(nextValue)); onValidityChange?.(true); onChange(nextValue); }; const addRootCondition = () => { if (!value) { updateExpressionValue(defaultWhenCondition()); return; } updateExpressionValue({ type: "and", left: value, right: defaultWhenCondition() }); }; const addRootGroup = () => { const group = defaultWhenGroup("or"); if (!value) { updateExpressionValue(group); return; } updateExpressionValue({ type: "and", left: value, right: group }); }; return (
When
updateExpressionDraft(event.currentTarget.value)} placeholder="Always" aria-invalid={Boolean(parseError)} aria-label="When expression" size="compact" font="mono" /> {unknownIdentifiers.length > 0 ? ( ) : null} {parseError ? (
{parseError}
) : null}
{value ? ( updateExpressionValue(undefined)} /> ) : (
)} {parseError ? (
Fix the expression above to continue editing visually.
) : null}
); } type KeybindingRowDraftState = { keyDraft: string; whenDraft: KeybindingWhenNode | undefined; isRecording: boolean; isWhenDraftValid: boolean; }; function createKeybindingRowDraft(row: KeybindingRow): KeybindingRowDraftState { return { keyDraft: row.key, whenDraft: row.binding.whenAst, isRecording: false, isWhenDraftValid: true, }; } function keybindingRowDraftReducer( state: KeybindingRowDraftState, patch: Partial, ): KeybindingRowDraftState { return { ...state, ...patch }; } function rowKeybindingTarget(row: KeybindingRow): ServerRemoveKeybindingInput { return { command: row.command, key: row.key, ...(row.when.trim().length > 0 ? { when: row.when } : {}), }; } function useKeybindingRowEditor({ row, allRows, onSave, }: { row: KeybindingRow; allRows: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; }) { const [draft, setDraft] = useReducer(keybindingRowDraftReducer, row, createKeybindingRowDraft); const pillRef = useRef(null); const { keyDraft, whenDraft, isRecording, isWhenDraftValid } = draft; const whenDraftExpression = whenAstToExpression(whenDraft); const isDirty = keyDraft !== row.key || whenDraftExpression !== row.when; const conflictLabels = keybindingConflictLabels(allRows, { rowId: row.id, key: keyDraft, when: whenDraftExpression, }); const save = () => { onSave({ command: row.command, key: keyDraft, when: whenDraftExpression.trim().length > 0 ? whenDraftExpression : undefined, replace: rowKeybindingTarget(row), }); }; const captureKeybinding = (event: KeyboardEvent) => { if (event.key === "Tab" && !isRecording) return; event.preventDefault(); // An unhandled Escape closes the settings page. if (event.key === "Escape") event.stopPropagation(); if (event.key === "Escape" && !(isRecording && commandRecordsEscape(row.command))) { const field = event.currentTarget; flushSync(() => setDraft({ keyDraft: row.key, isRecording: false })); // The field may unmount for the pill; focus would otherwise drop to the page. if (pillRef.current) pillRef.current.focus(); else field.blur(); return; } const next = keybindingFromKeyboardEvent(event.nativeEvent, navigator.platform, { layoutCharacter: true, }); if (!next) return; setDraft({ keyDraft: next, isRecording: false }); }; return { keyDraft, whenDraft, isRecording, isWhenDraftValid, whenDraftExpression, isDirty, conflictLabels, setDraft, save, captureKeybinding, pillRef, }; } type KeybindingRowEditor = ReturnType; interface KeybindingRowActions { allRows: ReadonlyArray; variables: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; } type KeybindingRowProps = KeybindingRowActions & { row: KeybindingRow; isSaving: boolean; anchorId?: string | undefined; }; function KeybindingKeyControl({ row, editor, isSaving, pillClassName, }: { row: KeybindingRow; editor: KeybindingRowEditor; isSaving: boolean; pillClassName?: string | undefined; }) { const { keyDraft, isRecording, isDirty, isWhenDraftValid, setDraft, save, captureKeybinding, pillRef, } = editor; const showPill = !isRecording && keyDraft === row.key && row.key.length > 0 && !isDirty; return ( <> {isDirty ? ( ) : null} {showPill ? ( ) : ( setDraft({ isRecording: true })} onBlur={() => setDraft({ isRecording: false })} onChange={(event) => setDraft({ keyDraft: event.currentTarget.value })} onKeyDown={captureKeybinding} /> )} ); } function WhenClauseControl({ label, expression, value, variables, onChange, onValidityChange, }: { label: string; expression: string; value: KeybindingWhenNode | undefined; variables: ReadonlyArray; onChange: (value: KeybindingWhenNode | undefined) => void; onValidityChange: (valid: boolean) => void; }) { // Closing unmounts the builder, which drops an unparsed draft and keeps the last valid value. const handleOpenChange = (open: boolean) => { if (!open) onValidityChange(true); }; return ( } aria-label={`Edit when clause for ${label}`} > {expression || "Always"} ); } function KeybindingRowMenu({ row, isSaving, onReset, onRemove, }: { row: KeybindingRow; isSaving: boolean; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; }) { const canReset = row.source === "Custom" && row.defaultKey !== null; const canRemove = row.source !== "Default"; if (!canReset && !canRemove) return null; return ( } > {canReset ? ( onReset(row)}> Reset to default ) : null} {canRemove ? ( onRemove(row)}> Remove ) : null} ); } function KeybindingSourceBadge({ source }: { source: KeybindingRow["source"] }) { if (source === "Default") return null; return ( {source} ); } function KeybindingRowTitle({ row }: { row: KeybindingRow }) { return ( }> {commandLabel(row.command)} {row.command} ); } function KeybindingRowWhen({ row, editor, variables, }: { row: KeybindingRow; editor: KeybindingRowEditor; variables: ReadonlyArray; }) { return ( When editor.setDraft({ whenDraft })} onValidityChange={(isWhenDraftValid) => editor.setDraft({ isWhenDraftValid })} /> ); } function KeybindingHoverRowMenu(props: { row: KeybindingRow; isSaving: boolean; onReset: (row: KeybindingRow) => void; onRemove: (row: KeybindingRow) => void; }) { return ( ); } function KeybindingSettingsRow(props: KeybindingRowProps) { const { row, isSaving, anchorId, allRows, variables, onSave, onReset, onRemove } = props; const editor = useKeybindingRowEditor({ row, allRows, onSave }); return ( } description={} control={
} /> ); } function useNewKeybindingDraft({ allRows, onSave, }: { allRows: ReadonlyArray; onSave: (input: ServerUpsertKeybindingInput) => void; }) { const [commandDraft, setCommandDraft] = useState(""); const [draft, setDraft] = useReducer(keybindingRowDraftReducer, { keyDraft: "", whenDraft: undefined, isRecording: false, isWhenDraftValid: true, }); const { keyDraft, whenDraft, isRecording, isWhenDraftValid } = draft; const whenDraftExpression = whenAstToExpression(whenDraft); const conflictLabels = keybindingConflictLabels(allRows, { rowId: "new", key: keyDraft, when: whenDraftExpression, }); const commandLabelText = commandDraft ? commandLabel(commandDraft) : "new keybinding"; const canSave = Boolean(commandDraft) && keyDraft.trim().length > 0 && isWhenDraftValid; const save = () => { if (!commandDraft) return; onSave({ command: commandDraft, key: keyDraft, ...(whenDraftExpression.trim().length > 0 ? { when: whenDraftExpression } : {}), }); }; const captureKeybinding = (event: KeyboardEvent) => { if (event.key === "Tab" && !isRecording) return; event.preventDefault(); // An unhandled Escape closes the settings page. if (event.key === "Escape") event.stopPropagation(); if (event.key === "Escape" && !(isRecording && commandRecordsEscape(commandDraft))) { setDraft({ keyDraft: "", isRecording: false }); event.currentTarget.blur(); return; } const next = keybindingFromKeyboardEvent(event.nativeEvent, navigator.platform, { layoutCharacter: true, }); if (!next) return; setDraft({ keyDraft: next, isRecording: false }); }; return { commandDraft, setCommandDraft, keyDraft, whenDraft, whenDraftExpression, isRecording, conflictLabels, commandLabelText, canSave, setDraft, save, captureKeybinding, }; } type NewKeybindingDraft = ReturnType; interface NewKeybindingProps { commandOptions: ReadonlyArray; allRows: ReadonlyArray; variables: ReadonlyArray; isSaving: boolean; onSave: (input: ServerUpsertKeybindingInput) => void; onCancel: () => void; } function NewKeybindingCommandSelect({ draft, commandOptions, className, }: { draft: NewKeybindingDraft; commandOptions: ReadonlyArray; className?: string | undefined; }) { return ( ); } function NewKeybindingKeyInput({ draft, autoFocus = false, className, }: { draft: NewKeybindingDraft; autoFocus?: boolean; className?: string | undefined; }) { return ( draft.setDraft({ isRecording: true })} onBlur={() => draft.setDraft({ isRecording: false })} onChange={(event) => draft.setDraft({ keyDraft: event.currentTarget.value })} onKeyDown={draft.captureKeybinding} /> ); } function NewKeybindingWhen({ draft, variables, }: { draft: NewKeybindingDraft; variables: ReadonlyArray; }) { return ( draft.setDraft({ whenDraft })} onValidityChange={(isWhenDraftValid) => draft.setDraft({ isWhenDraftValid })} /> ); } function NewKeybindingCancelIcon({ isSaving, onCancel, }: { isSaving: boolean; onCancel: () => void; }) { return ( } > Cancel ); } function NewKeybindingSettingsRow(props: NewKeybindingProps) { const { commandOptions, allRows, variables, isSaving, onSave, onCancel } = props; const draft = useNewKeybindingDraft({ allRows, onSave }); return ( When } control={
} /> ); } interface KeybindingsGroupsProps extends KeybindingRowActions { groups: ReadonlyArray; anchorIds: ReadonlyMap; savingCommand: KeybindingCommand | null; } function KeybindingsGroups(props: KeybindingsGroupsProps) { const { groups, anchorIds, savingCommand, ...rowActions } = props; return groups.map((group) => ( {group.rows.map((row) => ( ))} )); } function BrowserKeybindingNotice() { const message = "The browser may claim some shortcuts first. The desktop app receives them all."; return ( } /> {message} ); } function keybindingAnchorIds(rows: ReturnType) { const ids = new Map(); const seen = new Set(); for (const row of rows) { if (seen.has(row.command)) continue; seen.add(row.command); ids.set(row.id, keybindingSearchAnchorId(row.command)); } return ids; } // Cache rows in a separate hook so the compiler does not rebuild them per search keystroke. function useKeybindingCatalog() { const serverKeybindings = useSettingsScope().environment?.serverConfig?.keybindings; const merged = mergeWithDefaultKeybindings(serverKeybindings ?? []); return { commandOptions: buildKeybindingCommandOptions(merged), allRows: buildKeybindingRows(merged), }; } export function KeybindingsSettingsPanel() { const { environment: primaryEnvironment, connectedEnvironments } = useSettingsScope(); const canOpenKeybindingsFile = useEnvironmentScope( primaryEnvironment?.environmentId ?? null, AuthOrchestrationOperateScope, ); const writableIds = useEnvironmentsWithScope(connectedEnvironments, AuthSettingsWriteScope); const canWriteSettings = connectedEnvironments.length > 0 && connectedEnvironments.every((target) => writableIds.has(target.environmentId)); const keybindingsConfigPath = primaryEnvironment?.serverConfig?.keybindingsConfigPath ?? null; const availableEditors = primaryEnvironment?.serverConfig?.availableEditors ?? []; const upsertKeybinding = useAtomCommand(serverEnvironment.upsertKeybinding, { reportFailure: false, }); const removeKeybindingMutation = useAtomCommand(serverEnvironment.removeKeybinding, { reportFailure: false, }); const openInPreferredEditor = useOpenInPreferredEditor( primaryEnvironment?.environmentId ?? null, availableEditors, ); const [query, setQuery] = useState(""); const searchInputRef = useRef(null); const [savingCommand, setSavingCommand] = useState(null); const [isAddingBinding, setIsAddingBinding] = useState(false); const searchTargetId = useLocation({ select: (location) => location.hash.replace(/^#/, "") }); const [handledSearchTargetId, setHandledSearchTargetId] = useState(searchTargetId); if (searchTargetId !== handledSearchTargetId) { setHandledSearchTargetId(searchTargetId); if (searchTargetId.startsWith("keybinding-")) setQuery(""); } const whenVariables = buildWhenVariableOptions(); const { commandOptions, allRows } = useKeybindingCatalog(); const rows = filterKeybindingRows(allRows, query); const groups = groupKeybindingRows(rows); const anchorIds = keybindingAnchorIds(rows); const openKeybindingsFile = () => { if ( !keybindingsConfigPath || !primaryEnvironment || !readEnvironmentScope(primaryEnvironment.environmentId, AuthOrchestrationOperateScope) ) return; void (async () => { const result = await openInPreferredEditor(keybindingsConfigPath); if (result._tag === "Success" || isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); toastManager.add({ title: "Unable to open keybindings file", description: error instanceof Error ? error.message : "The keybindings file was not opened.", type: "error", }); })(); }; const saveKeybinding = (input: ServerUpsertKeybindingInput) => { if ( !primaryEnvironment || !connectedEnvironments.every((target) => readEnvironmentScope(target.environmentId, AuthSettingsWriteScope), ) ) return; setSavingCommand(input.command); const payload: ServerUpsertKeybindingInput = { command: input.command, key: input.key.trim(), ...(input.when?.trim() ? { when: input.when.trim() } : {}), ...(input.replace ? { replace: input.replace } : {}), }; void (async () => { const results = await Promise.all( connectedEnvironments.map((target) => upsertKeybinding({ environmentId: target.environmentId, input: payload }), ), ); setSavingCommand(null); const failed = results.find((result) => result._tag === "Failure"); if (!failed) { setIsAddingBinding(false); return; } if (!isAtomCommandInterrupted(failed)) { const error = squashAtomCommandFailure(failed); toastManager.add({ title: "Unable to save keybinding", description: error instanceof Error ? error.message : "The keybinding was not saved.", type: "error", }); } })(); }; const removeKeybinding = (row: KeybindingRow) => { if ( !primaryEnvironment || !connectedEnvironments.every((target) => readEnvironmentScope(target.environmentId, AuthSettingsWriteScope), ) ) return; setSavingCommand(row.command); void (async () => { const results = await Promise.all( connectedEnvironments.map((target) => removeKeybindingMutation({ environmentId: target.environmentId, input: rowKeybindingTarget(row), }), ), ); setSavingCommand(null); const result = results.find((entry) => entry._tag === "Failure") ?? results[0]; if (result?._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add({ title: "Unable to remove keybinding", description: error instanceof Error ? error.message : "The keybinding was not removed.", type: "error", }); } })(); }; const resetKeybinding = (row: KeybindingRow) => { if (!row.defaultKey) return; saveKeybinding({ command: row.command, key: row.defaultKey, when: row.defaultWhen.trim().length > 0 ? row.defaultWhen : undefined, replace: { command: row.command, key: row.key, ...(row.when.trim().length > 0 ? { when: row.when } : {}), }, }); }; const cancelAdd = () => setIsAddingBinding(false); const rowActions: KeybindingRowActions = { allRows, variables: whenVariables, onSave: saveKeybinding, onReset: resetKeybinding, onRemove: removeKeybinding, }; return ( : null} >
} /> Open keybindings.json
{!canWriteSettings ? (

{connectedEnvironments.length === 0 ? "Connect an environment to change keybindings." : "This connection can view keybindings but cannot change them."}

) : null}
{isAddingBinding ? ( ) : null} {groups.length > 0 ? ( ) : (
No keybindings match your search.
)}
); }