更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,149 +1,152 @@
import { c as _c } from "react/compiler-runtime";
import React, { createContext, type RefObject, useContext, useLayoutEffect, useMemo } from 'react';
import type { Key } from '../ink.js';
import { type ChordResolveResult, getBindingDisplayText, resolveKeyWithChordState } from './resolver.js';
import type { KeybindingContextName, ParsedBinding, ParsedKeystroke } from './types.js';
import React, {
createContext,
type RefObject,
useContext,
useLayoutEffect,
useMemo,
} from 'react'
import type { Key } from '../ink.js'
import {
type ChordResolveResult,
getBindingDisplayText,
resolveKeyWithChordState,
} from './resolver.js'
import type {
KeybindingContextName,
ParsedBinding,
ParsedKeystroke,
} from './types.js'
/** Handler registration for action callbacks */
type HandlerRegistration = {
action: string;
context: KeybindingContextName;
handler: () => void;
};
action: string
context: KeybindingContextName
handler: () => void
}
type KeybindingContextValue = {
/** Resolve a key input to an action name (with chord support) */
resolve: (input: string, key: Key, activeContexts: KeybindingContextName[]) => ChordResolveResult;
resolve: (
input: string,
key: Key,
activeContexts: KeybindingContextName[],
) => ChordResolveResult
/** Update the pending chord state */
setPendingChord: (pending: ParsedKeystroke[] | null) => void;
setPendingChord: (pending: ParsedKeystroke[] | null) => void
/** Get display text for an action (e.g., "ctrl+t") */
getDisplayText: (action: string, context: KeybindingContextName) => string | undefined;
getDisplayText: (
action: string,
context: KeybindingContextName,
) => string | undefined
/** All parsed bindings (for help display) */
bindings: ParsedBinding[];
bindings: ParsedBinding[]
/** Current pending chord keystrokes (null if not in a chord) */
pendingChord: ParsedKeystroke[] | null;
pendingChord: ParsedKeystroke[] | null
/** Currently active keybinding contexts (for priority resolution) */
activeContexts: Set<KeybindingContextName>;
activeContexts: Set<KeybindingContextName>
/** Register a context as active (call on mount) */
registerActiveContext: (context: KeybindingContextName) => void;
registerActiveContext: (context: KeybindingContextName) => void
/** Unregister a context (call on unmount) */
unregisterActiveContext: (context: KeybindingContextName) => void;
unregisterActiveContext: (context: KeybindingContextName) => void
/** Register a handler for an action (used by useKeybinding) */
registerHandler: (registration: HandlerRegistration) => () => void;
registerHandler: (registration: HandlerRegistration) => () => void
/** Invoke all handlers for an action (used by ChordInterceptor) */
invokeAction: (action: string) => boolean;
};
const KeybindingContext = createContext<KeybindingContextValue | null>(null);
invokeAction: (action: string) => boolean
}
const KeybindingContext = createContext<KeybindingContextValue | null>(null)
type ProviderProps = {
bindings: ParsedBinding[];
bindings: ParsedBinding[]
/** Ref for immediate access to pending chord (avoids React state delay) */
pendingChordRef: RefObject<ParsedKeystroke[] | null>;
pendingChordRef: RefObject<ParsedKeystroke[] | null>
/** State value for re-renders (UI updates) */
pendingChord: ParsedKeystroke[] | null;
setPendingChord: (pending: ParsedKeystroke[] | null) => void;
activeContexts: Set<KeybindingContextName>;
registerActiveContext: (context: KeybindingContextName) => void;
unregisterActiveContext: (context: KeybindingContextName) => void;
pendingChord: ParsedKeystroke[] | null
setPendingChord: (pending: ParsedKeystroke[] | null) => void
activeContexts: Set<KeybindingContextName>
registerActiveContext: (context: KeybindingContextName) => void
unregisterActiveContext: (context: KeybindingContextName) => void
/** Ref to handler registry (used by ChordInterceptor) */
handlerRegistryRef: RefObject<Map<string, Set<HandlerRegistration>>>;
children: React.ReactNode;
};
export function KeybindingProvider(t0) {
const $ = _c(24);
const {
bindings,
pendingChordRef,
pendingChord,
setPendingChord,
activeContexts,
registerActiveContext,
unregisterActiveContext,
handlerRegistryRef,
children
} = t0;
let t1;
if ($[0] !== bindings) {
t1 = (action, context) => getBindingDisplayText(action, context, bindings);
$[0] = bindings;
$[1] = t1;
} else {
t1 = $[1];
}
const getDisplay = t1;
let t2;
if ($[2] !== handlerRegistryRef) {
t2 = registration => {
const registry = handlerRegistryRef.current;
if (!registry) {
return _temp;
}
handlerRegistryRef: RefObject<Map<string, Set<HandlerRegistration>>>
children: React.ReactNode
}
export function KeybindingProvider({
bindings,
pendingChordRef,
pendingChord,
setPendingChord,
activeContexts,
registerActiveContext,
unregisterActiveContext,
handlerRegistryRef,
children,
}: ProviderProps): React.ReactNode {
const value = useMemo<KeybindingContextValue>(() => {
const getDisplay = (action: string, context: KeybindingContextName) =>
getBindingDisplayText(action, context, bindings)
// Register a handler for an action
const registerHandler = (registration: HandlerRegistration) => {
const registry = handlerRegistryRef.current
if (!registry) return () => {}
if (!registry.has(registration.action)) {
registry.set(registration.action, new Set());
registry.set(registration.action, new Set())
}
registry.get(registration.action).add(registration);
registry.get(registration.action)!.add(registration)
// Return unregister function
return () => {
const handlers = registry.get(registration.action);
const handlers = registry.get(registration.action)
if (handlers) {
handlers.delete(registration);
handlers.delete(registration)
if (handlers.size === 0) {
registry.delete(registration.action);
registry.delete(registration.action)
}
}
};
};
$[2] = handlerRegistryRef;
$[3] = t2;
} else {
t2 = $[3];
}
const registerHandler = t2;
let t3;
if ($[4] !== activeContexts || $[5] !== handlerRegistryRef) {
t3 = action_0 => {
const registry_0 = handlerRegistryRef.current;
if (!registry_0) {
return false;
}
const handlers_0 = registry_0.get(action_0);
if (!handlers_0 || handlers_0.size === 0) {
return false;
}
for (const registration_0 of handlers_0) {
if (activeContexts.has(registration_0.context)) {
registration_0.handler();
return true;
}
// Invoke all handlers for an action
const invokeAction = (action: string): boolean => {
const registry = handlerRegistryRef.current
if (!registry) return false
const handlers = registry.get(action)
if (!handlers || handlers.size === 0) return false
// Find handlers whose context is active
for (const registration of handlers) {
if (activeContexts.has(registration.context)) {
registration.handler()
return true
}
}
return false;
};
$[4] = activeContexts;
$[5] = handlerRegistryRef;
$[6] = t3;
} else {
t3 = $[6];
}
const invokeAction = t3;
let t4;
if ($[7] !== bindings || $[8] !== pendingChordRef) {
t4 = (input, key, contexts) => resolveKeyWithChordState(input, key, contexts, bindings, pendingChordRef.current);
$[7] = bindings;
$[8] = pendingChordRef;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== activeContexts || $[11] !== bindings || $[12] !== getDisplay || $[13] !== invokeAction || $[14] !== pendingChord || $[15] !== registerActiveContext || $[16] !== registerHandler || $[17] !== setPendingChord || $[18] !== t4 || $[19] !== unregisterActiveContext) {
t5 = {
resolve: t4,
return false
}
return {
// Use ref for immediate access to pending chord, avoiding React state delay
// This is critical for chord sequences where the second key might be pressed
// before React re-renders with the updated pendingChord state
resolve: (input, key, contexts) =>
resolveKeyWithChordState(
input,
key,
contexts,
bindings,
pendingChordRef.current,
),
setPendingChord,
getDisplayText: getDisplay,
bindings,
@@ -152,49 +155,42 @@ export function KeybindingProvider(t0) {
registerActiveContext,
unregisterActiveContext,
registerHandler,
invokeAction
};
$[10] = activeContexts;
$[11] = bindings;
$[12] = getDisplay;
$[13] = invokeAction;
$[14] = pendingChord;
$[15] = registerActiveContext;
$[16] = registerHandler;
$[17] = setPendingChord;
$[18] = t4;
$[19] = unregisterActiveContext;
$[20] = t5;
} else {
t5 = $[20];
}
const value = t5;
let t6;
if ($[21] !== children || $[22] !== value) {
t6 = <KeybindingContext.Provider value={value}>{children}</KeybindingContext.Provider>;
$[21] = children;
$[22] = value;
$[23] = t6;
} else {
t6 = $[23];
}
return t6;
invokeAction,
}
}, [
bindings,
pendingChordRef,
pendingChord,
setPendingChord,
activeContexts,
registerActiveContext,
unregisterActiveContext,
handlerRegistryRef,
])
return (
<KeybindingContext.Provider value={value}>
{children}
</KeybindingContext.Provider>
)
}
function _temp() {}
export function useKeybindingContext() {
const ctx = useContext(KeybindingContext);
export function useKeybindingContext(): KeybindingContextValue {
const ctx = useContext(KeybindingContext)
if (!ctx) {
throw new Error("useKeybindingContext must be used within KeybindingProvider");
throw new Error(
'useKeybindingContext must be used within KeybindingProvider',
)
}
return ctx;
return ctx
}
/**
* Optional hook that returns undefined outside of KeybindingProvider.
* Useful for components that may render before provider is available.
*/
export function useOptionalKeybindingContext() {
return useContext(KeybindingContext);
export function useOptionalKeybindingContext(): KeybindingContextValue | null {
return useContext(KeybindingContext)
}
/**
@@ -212,31 +208,18 @@ export function useOptionalKeybindingContext() {
* }
* ```
*/
export function useRegisterKeybindingContext(context, t0) {
const $ = _c(5);
const isActive = t0 === undefined ? true : t0;
const keybindingContext = useOptionalKeybindingContext();
let t1;
let t2;
if ($[0] !== context || $[1] !== isActive || $[2] !== keybindingContext) {
t1 = () => {
if (!keybindingContext || !isActive) {
return;
}
keybindingContext.registerActiveContext(context);
return () => {
keybindingContext.unregisterActiveContext(context);
};
};
t2 = [context, keybindingContext, isActive];
$[0] = context;
$[1] = isActive;
$[2] = keybindingContext;
$[3] = t1;
$[4] = t2;
} else {
t1 = $[3];
t2 = $[4];
}
useLayoutEffect(t1, t2);
export function useRegisterKeybindingContext(
context: KeybindingContextName,
isActive: boolean = true,
): void {
const keybindingContext = useOptionalKeybindingContext()
useLayoutEffect(() => {
if (!keybindingContext || !isActive) return
keybindingContext.registerActiveContext(context)
return () => {
keybindingContext.unregisterActiveContext(context)
}
}, [context, keybindingContext, isActive])
}

View File

@@ -1,4 +1,3 @@
import { c as _c } from "react/compiler-runtime";
/**
* Setup utilities for integrating KeybindingProvider into the app.
*
@@ -7,30 +6,40 @@ import { c as _c } from "react/compiler-runtime";
* user-defined bindings from ~/.claude/keybindings.json, with hot-reload
* support when the file changes.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNotifications } from '../context/notifications.js';
import type { InputEvent } from '../ink/events/input-event.js';
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useNotifications } from '../context/notifications.js'
import type { InputEvent } from '../ink/events/input-event.js'
// ChordInterceptor intentionally uses useInput to intercept all keystrokes before
// other handlers process them - this is required for chord sequence support
// eslint-disable-next-line custom-rules/prefer-use-keybindings
import { type Key, useInput } from '../ink.js';
import { count } from '../utils/array.js';
import { logForDebugging } from '../utils/debug.js';
import { plural } from '../utils/stringUtils.js';
import { KeybindingProvider } from './KeybindingContext.js';
import { initializeKeybindingWatcher, type KeybindingsLoadResult, loadKeybindingsSyncWithWarnings, subscribeToKeybindingChanges } from './loadUserBindings.js';
import { resolveKeyWithChordState } from './resolver.js';
import type { KeybindingContextName, ParsedBinding, ParsedKeystroke } from './types.js';
import type { KeybindingWarning } from './validate.js';
import { type Key, useInput } from '../ink.js'
import { count } from '../utils/array.js'
import { logForDebugging } from '../utils/debug.js'
import { plural } from '../utils/stringUtils.js'
import { KeybindingProvider } from './KeybindingContext.js'
import {
initializeKeybindingWatcher,
type KeybindingsLoadResult,
loadKeybindingsSyncWithWarnings,
subscribeToKeybindingChanges,
} from './loadUserBindings.js'
import { resolveKeyWithChordState } from './resolver.js'
import type {
KeybindingContextName,
ParsedBinding,
ParsedKeystroke,
} from './types.js'
import type { KeybindingWarning } from './validate.js'
/**
* Timeout for chord sequences in milliseconds.
* If the user doesn't complete the chord within this time, it's cancelled.
*/
const CHORD_TIMEOUT_MS = 1000;
const CHORD_TIMEOUT_MS = 1000
type Props = {
children: React.ReactNode;
};
children: React.ReactNode
}
/**
* Keybinding provider with default + user bindings and hot-reload support.
@@ -56,156 +65,179 @@ type Props = {
* Display keybinding warnings to the user via notifications.
* Shows a brief message pointing to /doctor for details.
*/
function useKeybindingWarnings(warnings, isReload) {
const $ = _c(9);
const {
addNotification,
removeNotification
} = useNotifications();
let t0;
if ($[0] !== addNotification || $[1] !== removeNotification || $[2] !== warnings) {
t0 = () => {
if (warnings.length === 0) {
removeNotification("keybinding-config-warning");
return;
}
const errorCount = count(warnings, _temp);
const warnCount = count(warnings, _temp2);
let message;
if (errorCount > 0 && warnCount > 0) {
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")} and ${warnCount} ${plural(warnCount, "warning")}`;
} else {
if (errorCount > 0) {
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")}`;
} else {
message = `Found ${warnCount} keybinding ${plural(warnCount, "warning")}`;
}
}
message = message + " \xB7 /doctor for details";
addNotification({
key: "keybinding-config-warning",
text: message,
color: errorCount > 0 ? "error" : "warning",
priority: errorCount > 0 ? "immediate" : "high",
timeoutMs: 60000
});
};
$[0] = addNotification;
$[1] = removeNotification;
$[2] = warnings;
$[3] = t0;
} else {
t0 = $[3];
}
let t1;
if ($[4] !== addNotification || $[5] !== isReload || $[6] !== removeNotification || $[7] !== warnings) {
t1 = [warnings, isReload, addNotification, removeNotification];
$[4] = addNotification;
$[5] = isReload;
$[6] = removeNotification;
$[7] = warnings;
$[8] = t1;
} else {
t1 = $[8];
}
useEffect(t0, t1);
function useKeybindingWarnings(
warnings: KeybindingWarning[],
isReload: boolean,
): void {
const { addNotification, removeNotification } = useNotifications()
useEffect(() => {
const notificationKey = 'keybinding-config-warning'
if (warnings.length === 0) {
removeNotification(notificationKey)
return
}
const errorCount = count(warnings, w => w.severity === 'error')
const warnCount = count(warnings, w => w.severity === 'warning')
let message: string
if (errorCount > 0 && warnCount > 0) {
message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')} and ${warnCount} ${plural(warnCount, 'warning')}`
} else if (errorCount > 0) {
message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')}`
} else {
message = `Found ${warnCount} keybinding ${plural(warnCount, 'warning')}`
}
message += ' · /doctor for details'
addNotification({
key: notificationKey,
text: message,
color: errorCount > 0 ? 'error' : 'warning',
priority: errorCount > 0 ? 'immediate' : 'high',
// Keep visible for 60 seconds like settings errors
timeoutMs: 60000,
})
}, [warnings, isReload, addNotification, removeNotification])
}
function _temp2(w_0) {
return w_0.severity === "warning";
}
function _temp(w) {
return w.severity === "error";
}
export function KeybindingSetup({
children
}: Props): React.ReactNode {
export function KeybindingSetup({ children }: Props): React.ReactNode {
// Load bindings synchronously for initial render
const [{
bindings,
warnings
}, setLoadResult] = useState<KeybindingsLoadResult>(() => {
const result = loadKeybindingsSyncWithWarnings();
logForDebugging(`[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`);
return result;
});
const [{ bindings, warnings }, setLoadResult] =
useState<KeybindingsLoadResult>(() => {
const result = loadKeybindingsSyncWithWarnings()
logForDebugging(
`[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`,
)
return result
})
// Track if this is a reload (not initial load)
const [isReload, setIsReload] = useState(false);
const [isReload, setIsReload] = useState(false)
// Display warnings via notifications
useKeybindingWarnings(warnings, isReload);
useKeybindingWarnings(warnings, isReload)
// Chord state management - use ref for immediate access, state for re-renders
// The ref is used by resolve() to get the current value without waiting for re-render
// The state is used to trigger re-renders when needed (e.g., for UI updates)
const pendingChordRef = useRef<ParsedKeystroke[] | null>(null);
const [pendingChord, setPendingChordState] = useState<ParsedKeystroke[] | null>(null);
const chordTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const pendingChordRef = useRef<ParsedKeystroke[] | null>(null)
const [pendingChord, setPendingChordState] = useState<
ParsedKeystroke[] | null
>(null)
const chordTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// Handler registry for action callbacks (used by ChordInterceptor to invoke handlers)
const handlerRegistryRef = useRef(new Map<string, Set<{
action: string;
context: KeybindingContextName;
handler: () => void;
}>>());
const handlerRegistryRef = useRef(
new Map<
string,
Set<{
action: string
context: KeybindingContextName
handler: () => void
}>
>(),
)
// Active context tracking for keybinding priority resolution
// Using a ref instead of state for synchronous updates - input handlers need
// to see the current value immediately, not after a React render cycle.
const activeContextsRef = useRef<Set<KeybindingContextName>>(new Set());
const registerActiveContext = useCallback((context: KeybindingContextName) => {
activeContextsRef.current.add(context);
}, []);
const unregisterActiveContext = useCallback((context_0: KeybindingContextName) => {
activeContextsRef.current.delete(context_0);
}, []);
const activeContextsRef = useRef<Set<KeybindingContextName>>(new Set())
const registerActiveContext = useCallback(
(context: KeybindingContextName) => {
activeContextsRef.current.add(context)
},
[],
)
const unregisterActiveContext = useCallback(
(context: KeybindingContextName) => {
activeContextsRef.current.delete(context)
},
[],
)
// Clear chord timeout when component unmounts or chord changes
const clearChordTimeout = useCallback(() => {
if (chordTimeoutRef.current) {
clearTimeout(chordTimeoutRef.current);
chordTimeoutRef.current = null;
clearTimeout(chordTimeoutRef.current)
chordTimeoutRef.current = null
}
}, []);
}, [])
// Wrapper for setPendingChord that manages timeout and syncs ref+state
const setPendingChord = useCallback((pending: ParsedKeystroke[] | null) => {
clearChordTimeout();
if (pending !== null) {
// Set timeout to cancel chord if not completed
chordTimeoutRef.current = setTimeout((pendingChordRef_0, setPendingChordState_0) => {
logForDebugging('[keybindings] Chord timeout - cancelling');
pendingChordRef_0.current = null;
setPendingChordState_0(null);
}, CHORD_TIMEOUT_MS, pendingChordRef, setPendingChordState);
}
const setPendingChord = useCallback(
(pending: ParsedKeystroke[] | null) => {
clearChordTimeout()
if (pending !== null) {
// Set timeout to cancel chord if not completed
chordTimeoutRef.current = setTimeout(
(pendingChordRef, setPendingChordState) => {
logForDebugging('[keybindings] Chord timeout - cancelling')
pendingChordRef.current = null
setPendingChordState(null)
},
CHORD_TIMEOUT_MS,
pendingChordRef,
setPendingChordState,
)
}
// Update ref immediately for synchronous access in resolve()
pendingChordRef.current = pending
// Update state to trigger re-renders for UI updates
setPendingChordState(pending)
},
[clearChordTimeout],
)
// Update ref immediately for synchronous access in resolve()
pendingChordRef.current = pending;
// Update state to trigger re-renders for UI updates
setPendingChordState(pending);
}, [clearChordTimeout]);
useEffect(() => {
// Initialize file watcher (idempotent - only runs once)
void initializeKeybindingWatcher();
void initializeKeybindingWatcher()
// Subscribe to changes
const unsubscribe = subscribeToKeybindingChanges(result_0 => {
const unsubscribe = subscribeToKeybindingChanges(result => {
// Any callback invocation is a reload since initial load happens
// synchronously in useState, not via this subscription
setIsReload(true);
setLoadResult(result_0);
logForDebugging(`[keybindings] Reloaded: ${result_0.bindings.length} bindings, ${result_0.warnings.length} warnings`);
});
setIsReload(true)
setLoadResult(result)
logForDebugging(
`[keybindings] Reloaded: ${result.bindings.length} bindings, ${result.warnings.length} warnings`,
)
})
return () => {
unsubscribe();
clearChordTimeout();
};
}, [clearChordTimeout]);
return <KeybindingProvider bindings={bindings} pendingChordRef={pendingChordRef} pendingChord={pendingChord} setPendingChord={setPendingChord} activeContexts={activeContextsRef.current} registerActiveContext={registerActiveContext} unregisterActiveContext={unregisterActiveContext} handlerRegistryRef={handlerRegistryRef}>
<ChordInterceptor bindings={bindings} pendingChordRef={pendingChordRef} setPendingChord={setPendingChord} activeContexts={activeContextsRef.current} handlerRegistryRef={handlerRegistryRef} />
unsubscribe()
clearChordTimeout()
}
}, [clearChordTimeout])
return (
<KeybindingProvider
bindings={bindings}
pendingChordRef={pendingChordRef}
pendingChord={pendingChord}
setPendingChord={setPendingChord}
activeContexts={activeContextsRef.current}
registerActiveContext={registerActiveContext}
unregisterActiveContext={unregisterActiveContext}
handlerRegistryRef={handlerRegistryRef}
>
<ChordInterceptor
bindings={bindings}
pendingChordRef={pendingChordRef}
setPendingChord={setPendingChord}
activeContexts={activeContextsRef.current}
handlerRegistryRef={handlerRegistryRef}
/>
{children}
</KeybindingProvider>;
</KeybindingProvider>
)
}
/**
@@ -219,89 +251,131 @@ export function KeybindingSetup({
* system could recognize it as completing a chord.
*/
type HandlerRegistration = {
action: string;
context: KeybindingContextName;
handler: () => void;
};
function ChordInterceptor(t0) {
const $ = _c(6);
const {
bindings,
pendingChordRef,
setPendingChord,
activeContexts,
handlerRegistryRef
} = t0;
let t1;
if ($[0] !== activeContexts || $[1] !== bindings || $[2] !== handlerRegistryRef || $[3] !== pendingChordRef || $[4] !== setPendingChord) {
t1 = (input, key, event) => {
action: string
context: KeybindingContextName
handler: () => void
}
function ChordInterceptor({
bindings,
pendingChordRef,
setPendingChord,
activeContexts,
handlerRegistryRef,
}: {
bindings: ParsedBinding[]
pendingChordRef: React.RefObject<ParsedKeystroke[] | null>
setPendingChord: (pending: ParsedKeystroke[] | null) => void
activeContexts: Set<KeybindingContextName>
handlerRegistryRef: React.RefObject<Map<string, Set<HandlerRegistration>>>
}): null {
const handleInput = useCallback(
(input: string, key: Key, event: InputEvent) => {
// Wheel events can never start chord sequences — scroll:lineUp/Down are
// single-key bindings handled by per-component useKeybindings hooks, not
// here. Skip the registry scan. Mid-chord wheel still falls through so
// scrolling cancels the pending chord like any other non-matching key.
if ((key.wheelUp || key.wheelDown) && pendingChordRef.current === null) {
return;
return
}
const registry = handlerRegistryRef.current;
const handlerContexts = new Set();
// Build context list from registered handlers + activeContexts + Global
// This ensures we can resolve chords for all contexts that have handlers
const registry = handlerRegistryRef.current
const handlerContexts = new Set<KeybindingContextName>()
if (registry) {
for (const handlers of registry.values()) {
for (const registration of handlers) {
handlerContexts.add(registration.context);
handlerContexts.add(registration.context)
}
}
}
const contexts = [...handlerContexts, ...activeContexts, "Global"];
const wasInChord = pendingChordRef.current !== null;
const result = resolveKeyWithChordState(input, key, contexts, bindings, pendingChordRef.current);
bb23: switch (result.type) {
case "chord_started":
{
setPendingChord(result.pending);
event.stopImmediatePropagation();
break bb23;
}
case "match":
{
setPendingChord(null);
if (wasInChord) {
const contextsSet = new Set(contexts);
if (registry) {
const handlers_0 = registry.get(result.action);
if (handlers_0 && handlers_0.size > 0) {
for (const registration_0 of handlers_0) {
if (contextsSet.has(registration_0.context)) {
registration_0.handler();
event.stopImmediatePropagation();
break;
}
const contexts: KeybindingContextName[] = [
...handlerContexts,
...activeContexts,
'Global',
]
// Track whether we're completing a chord (pending was non-null)
const wasInChord = pendingChordRef.current !== null
// Check if this keystroke is part of a chord sequence
const result = resolveKeyWithChordState(
input,
key,
contexts,
bindings,
pendingChordRef.current,
)
switch (result.type) {
case 'chord_started':
// This key starts a chord - store pending state and stop propagation
setPendingChord(result.pending)
event.stopImmediatePropagation()
break
case 'match': {
// Clear pending state
setPendingChord(null)
// Only invoke handlers and stop propagation for chord completions
// (multi-keystroke sequences). Single-keystroke matches should propagate
// to per-hook handlers to avoid interfering with other input handling
// (e.g., Enter needs to reach useTypeahead for autocomplete acceptance
// before the submit handler fires).
if (wasInChord) {
// Find and invoke the handler for this action
// We need to check that the handler's context is in our resolved contexts
// (which includes handlerContexts + activeContexts + Global)
const contextsSet = new Set(contexts)
if (registry) {
const handlers = registry.get(result.action)
if (handlers && handlers.size > 0) {
// Find handlers whose context is in our resolved contexts
for (const registration of handlers) {
if (contextsSet.has(registration.context)) {
registration.handler()
event.stopImmediatePropagation()
break // Only invoke the first matching handler
}
}
}
}
break bb23;
}
case "chord_cancelled":
{
setPendingChord(null);
event.stopImmediatePropagation();
break bb23;
}
case "unbound":
{
setPendingChord(null);
event.stopImmediatePropagation();
break bb23;
}
case "none":
break
}
case 'chord_cancelled':
// Invalid key during chord - clear pending state and swallow the
// keystroke so it doesn't propagate as a standalone action
// (e.g., ctrl+x ctrl+c should not fire app:interrupt).
setPendingChord(null)
event.stopImmediatePropagation()
break
case 'unbound':
// Key is explicitly unbound - clear pending state and swallow
// the keystroke (it was part of a chord sequence).
setPendingChord(null)
event.stopImmediatePropagation()
break
case 'none':
// No chord involvement - let other handlers process
break
}
};
$[0] = activeContexts;
$[1] = bindings;
$[2] = handlerRegistryRef;
$[3] = pendingChordRef;
$[4] = setPendingChord;
$[5] = t1;
} else {
t1 = $[5];
}
const handleInput = t1;
useInput(handleInput);
return null;
},
[
bindings,
pendingChordRef,
setPendingChord,
activeContexts,
handlerRegistryRef,
],
)
useInput(handleInput)
return null
}