diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000000..14d86ad6230 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/memories/leader-chord-architecture-design.md b/.serena/memories/leader-chord-architecture-design.md new file mode 100644 index 00000000000..a3657154282 --- /dev/null +++ b/.serena/memories/leader-chord-architecture-design.md @@ -0,0 +1,122 @@ +# Leader State Machine & Chord Parsing Architecture Design + +## Overview + +Design a robust state machine for handling leader keys and chord parsing in the TUI editor, building on the existing keybind system. + +## State Machine Architecture + +### Core States +```typescript +type KeyState = + | 'idle' // Normal key processing + | 'leader' // Leader key pressed, waiting for sequence + | 'chording' // Multiple keys held (chord detection) + | 'sequencing' // Building key sequence after leader + | 'timeout' // Leader mode expired + | 'matched' // Complete sequence matched + | 'failed' // No matching sequence found +``` + +### State Transitions +- `idle` → `leader` on leader key press +- `idle` → `chording` on multiple simultaneous keys +- `leader` → `sequencing` on first key after leader +- `sequencing` → `matched` on complete sequence match +- `sequencing` → `failed` on no match +- `leader` → `timeout` after 2 seconds +- `chording` → `matched` on chord match +- `chording` → `failed` on no chord match + +## Chord Parsing System + +### Chord Detection +- Track key down/up events +- Maintain active keys set +- Detect simultaneous key combinations +- Handle key release order variations + +### Chord Types +1. **Simple Chords**: 2-3 keys simultaneously (e.g., `gg`, `dd`) +2. **Modified Chords**: Modifiers + keys (e.g., `ctrl+xx`) +3. **Leader Chords**: Leader + simultaneous keys (e.g., `gg`) + +## Implementation Components + +### 1. KeyStateMachine +Main state machine orchestrating key handling +- State management +- Transition logic +- Timeout handling +- Event dispatch + +### 2. ChordDetector +Handles chord detection and parsing +- Key press tracking +- Simultaneous key detection +- Chord pattern matching + +### 3. SequenceBuilder +Builds and validates key sequences +- Sequence accumulation +- Pattern matching +- Completion detection + +### 4. Enhanced KeybindContext +Integrates with existing keybind system +- Backward compatibility +- Enhanced matching +- State-aware key processing + +## Integration Points + +### Existing Keybind System +- Extend `Keybind.Info` with chord information +- Enhance `Keybind.parse()` for chord syntax +- Update `Keybind.match()` for chord matching + +### TUI Context +- Enhance `keybind.tsx` with state machine +- Maintain focus management +- Add visual feedback for leader/chord modes + +## Configuration Schema + +### Chord Syntax Examples +``` +# Simple chords +"gg" # Press g twice +"dd" # Press d twice + +# Modified chords +"ctrl+xx" # Hold ctrl + press x twice +"shift+ww" # Hold shift + press w twice + +# Leader chords +"gg" # Leader + g twice +"dd" # Leader + d twice + +# Mixed sequences +"g d" # Leader + g then d +``` + +## Testing Strategy + +### Unit Tests +- State machine transitions +- Chord detection logic +- Pattern matching +- Timeout handling + +### Integration Tests +- Full keybind workflow +- Focus management +- Visual feedback +- Performance under rapid key presses + +## Performance Considerations + +- Efficient key tracking with Sets +- Debounced timeout handling +- Minimal state updates +- Fast pattern matching \ No newline at end of file diff --git a/.serena/memories/leader-state-machine-research.md b/.serena/memories/leader-state-machine-research.md new file mode 100644 index 00000000000..7009d435c0f --- /dev/null +++ b/.serena/memories/leader-state-machine-research.md @@ -0,0 +1,66 @@ +# Leader State Machine Research Summary + +## Current Implementation Analysis + +### Existing Leader Key System +The current leader implementation in `packages/opencode/src/cli/cmd/tui/context/keybind.tsx` provides: + +1. **Basic Leader State Management**: + - Simple boolean `leader` state in a SolidJS store + - 2-second timeout with `clearTimeout` + - Focus management during leader mode + +2. **Leader Activation Logic**: + ```typescript + if (!store.leader && result.match("leader", evt)) { + leader(true) + return + } + ``` + +3. **Focus Handling**: + - Stores current focused renderable before leader activation + - Blurs current element during leader mode + - Restores focus after leader completion + +### Current Keybind Parsing +From `packages/opencode/src/util/keybind.ts`: + +1. **Leader Syntax Support**: + - Parses `` notation + - Converts to `leader: true` in Keybind.Info + - Supports combinations like `f`, `ctrl+g` + +2. **Limitations**: + - Only supports single leader + one key combinations + - No multi-key chord parsing (e.g., leader + a + b) + - No state machine for complex sequences + +### Configuration Structure +From `packages/opencode/src/config/config.ts`: +- Leader key defaults to `ctrl+x` +- Extensive keybind definitions using leader syntax +- All major TUI commands use leader + key combinations + +## Identified Gaps + +1. **No True State Machine**: Current implementation is a simple timeout-based system +2. **Limited Chord Support**: Only leader + single key, not multi-key sequences +3. **No Nested States**: Cannot handle complex chord hierarchies +4. **Missing Visual Feedback**: No indication of partial chord progress + +## TUI Input Flow Analysis + +From the TUI component analysis: +- Keyboard events flow through `useKeyboard` hook from `@opentui/solid` +- Events are processed in `keybind.tsx` context +- Individual components handle their own keybinds +- Leader state affects focus across the entire TUI + +## Requirements for Enhancement + +1. **State Machine Architecture**: Replace simple boolean with proper state machine +2. **Multi-Key Chord Parsing**: Support sequences like leader + a + b +3. **Visual Feedback**: Show chord progress to user +4. **Configurable Timeouts**: Different timeouts for different contexts +5. **Chord Conflict Resolution**: Handle overlapping chord definitions \ No newline at end of file diff --git a/.serena/memories/native-library-detection-strategy.md b/.serena/memories/native-library-detection-strategy.md new file mode 100644 index 00000000000..e716fd97e3d --- /dev/null +++ b/.serena/memories/native-library-detection-strategy.md @@ -0,0 +1,38 @@ +## Native Library Detection Strategy + +### Key Findings from Code Analysis + +1. **Main Entry Points**: + - `TuiThreadCommand.handler()` in `thread.ts` - Main TUI entry point + - `tui()` function in `app.tsx:88-151` - Core TUI rendering logic + - Both call `render()` from `@opentui/solid` + +2. **Current Error Handling**: + - ErrorBoundary in `app.tsx:116-150` catches render errors + - Global handlers for unhandledRejection/uncaughtException in `thread.ts` + - No specific detection for native library loading failures + +3. **Native Library Structure**: + - `@opentui/core` has platform-specific optional dependencies + - Platform packages: `@opentui/core-{os}-{arch}` (e.g., `@opentui/core-linux-x64`) + - Build process downloads platform-specific binaries in `script/build.ts:42-45` + +4. **Failure Points**: + - Import failures for `@opentui/solid` or `@opentui/core` + - Native binary loading failures during platform-specific library import + - Missing platform-specific packages + - Terminal compatibility issues + +### Detection Strategy + +1. **Import-Time Detection**: Create a function to test importing native modules +2. **Platform Validation**: Check if platform-specific native libraries exist +3. **Runtime Detection**: Test renderer initialization before full TUI startup +4. **Graceful Messaging**: Provide clear error messages with fallback options + +### Implementation Plan + +1. Create `detectNativeLibrarySupport()` utility function +2. Wrap TUI entry points with detection logic +3. Enhance error handling with native library-specific messages +4. Add fallback suggestions (web UI, different terminal, etc.) \ No newline at end of file diff --git a/.serena/memories/windows-git-bash-rendering-solution-plan.md b/.serena/memories/windows-git-bash-rendering-solution-plan.md new file mode 100644 index 00000000000..3fbadfb83b7 --- /dev/null +++ b/.serena/memories/windows-git-bash-rendering-solution-plan.md @@ -0,0 +1,42 @@ +# Windows Git Bash Rendering Parity Solution Plan + +## Problem Summary +Windows Git Bash has rendering parity issues due to: +1. Inconsistent ANSI escape sequence support +2. Missing Git Bash-specific terminal detection +3. No fallback rendering for limited terminals +4. Path display format inconsistencies + +## Solution Architecture + +### 1. Git Bash Detection Utility +Create utility to detect Git Bash environment specifically: +- Check for MINGW/MSYS/CYGWIN in platform +- Verify SHELL environment variable points to bash +- Detect Git Bash specific terminal capabilities + +### 2. Terminal Capability Detection +Test for ANSI support in Git Bash: +- Background detection sequence fallback +- Color support detection +- Mouse support detection +- Clipboard support detection + +### 3. Platform-Specific Rendering Fallbacks +Implement simplified rendering for Git Bash: +- Reduced color palette fallback +- Simplified text formatting +- Path normalization for display +- Enhanced error handling + +### 4. Implementation Points +- `packages/opencode/src/util/platform.ts` - Git Bash detection +- `packages/opencode/src/util/terminal-capabilities.ts` - Capability detection +- `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` - Rendering fallbacks +- `packages/opencode/src/config/markdown.ts` - Path normalization + +## Testing Strategy +- Test in actual Git Bash environment +- Verify rendering parity with other terminals +- Test path display normalization +- Validate fallback rendering works correctly \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000000..0724a4556bd --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,84 @@ +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp csharp_omnisharp +# dart elixir elm erlang fortran go +# haskell java julia kotlin lua markdown +# nix perl php python python_jedi r +# rego ruby ruby_solargraph rust scala swift +# terraform typescript typescript_vts zig +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# Special requirements: +# - csharp: Requires the presence of a .sln file in the project folder. +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- typescript + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "2-3-readline-vim-parity-in-list-select-inputs" +included_optional_tools: [] diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index f0cbfba5e08..ba88a5e8fd4 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,6 +1,7 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" +import { showFallbackUI } from "./fallback-ui" import { RouteProvider, useRoute, type Route } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal } from "solid-js" import { Installation } from "@/installation" @@ -86,6 +87,80 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { }) } +async function detectNativeLibrarySupport(): Promise<{ supported: boolean; error?: string; suggestions?: string[] }> { + try { + // Test importing the core modules + const core = await import("@opentui/core") + const solid = await import("@opentui/solid") + + // Test if we can access the render function + if (typeof solid.render !== "function") { + return { + supported: false, + error: "Render function not available in @opentui/solid", + suggestions: [ + "Try updating your dependencies: bun install", + "Check if your platform is supported: @opentui/core requires platform-specific native libraries", + "Use the web UI instead: opencode web" + ] + } + } + + // Test basic terminal capabilities + if (!process.stdout.isTTY && !process.env.FORCE_TUI) { + return { + supported: false, + error: "TUI requires a terminal (TTY)", + suggestions: [ + "Run in a proper terminal environment", + "Use FORCE_TUI=1 to override this check", + "Use the web UI instead: opencode web" + ] + } + } + + return { supported: true } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + // Check for common native library issues + if (errorMessage.includes("Cannot find module")) { + return { + supported: false, + error: `Native library not found: ${errorMessage}`, + suggestions: [ + "Install missing dependencies: bun install", + "Check if your platform (${process.platform}-${process.arch}) is supported", + "Try rebuilding: bun run build", + "Use the web UI instead: opencode web" + ] + } + } + + if (errorMessage.includes("DYLD") || errorMessage.includes("DLL")) { + return { + supported: false, + error: `Native library loading failed: ${errorMessage}`, + suggestions: [ + "Check system library dependencies", + "Try reinstalling: bun install --force", + "Use the web UI instead: opencode web" + ] + } + } + + return { + supported: false, + error: `Native library error: ${errorMessage}`, + suggestions: [ + "Check the error message above for specific issues", + "Try updating dependencies: bun install", + "Use the web UI instead: opencode web" + ] + } + } +} + export function tui(input: { url: string sessionID?: string @@ -96,6 +171,37 @@ export function tui(input: { }) { // promise to prevent immediate exit return new Promise(async (resolve) => { + // Detect native library support first + const detection = await detectNativeLibrarySupport() + if (!detection.supported) { + showFallbackUI({ + error: detection.error || "Unknown error loading native render library", + suggestions: detection.suggestions || [ + "Try rebuilding: bun run build", + "Check if your platform is supported", + "Use the web UI instead: opencode web" + ], + onRetry: () => { + // Retry the TUI startup + tui(input).then(resolve).catch(() => resolve()) + }, + onExit: () => { + input.onExit?.().then(() => resolve()).catch(() => resolve()) + } + }) + return + if (detection.suggestions && detection.suggestions.length > 0) { + console.error("💡 Suggestions:") + detection.suggestions.forEach((suggestion, index) => { + console.error(` ${index + 1}. ${suggestion}`) + }) + } + console.error("") + await input.onExit?.() + resolve() + return + } + const mode = await getTerminalBackgroundColor() const routeData: Route | undefined = input.sessionID @@ -110,7 +216,31 @@ export function tui(input: { resolve() } - render( + // Dynamic import of render function after detection passes + let renderModule: typeof import("@opentui/solid") | null = null + try { + renderModule = await import("@opentui/solid") + } catch (error) { + console.error(`\n❌ Failed to load TUI renderer: ${error instanceof Error ? error.message : String(error)}\n`) + console.error("💡 Suggestions:") + console.error(" 1. Try rebuilding: bun run build") + console.error(" 2. Use the web UI instead: opencode web") + await input.onExit?.() + resolve() + return + } + + if (!renderModule) { + console.error("\n❌ TUI renderer module not available\n") + console.error("💡 Suggestions:") + console.error(" 1. Try reinstalling dependencies: bun install") + console.error(" 2. Use the web UI instead: opencode web") + await input.onExit?.() + resolve() + return + } + + renderModule.render( () => { return ( props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0], ) + + const processedText = createMemo(() => { + if (!text()) return "" + const rawText = text()!.text + const sanitized = sanitizeTextForTerminal(rawText) + return processAnsiForTerminal(sanitized) + }) const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : []))) const sync = useSync() const { theme } = useTheme() @@ -703,7 +710,7 @@ function UserMessage(props: { borderColor={color()} flexShrink={0} > - {text()?.text} + {processedText()} @@ -854,14 +861,21 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage } function TextPart(props: { part: TextPart; message: AssistantMessage }) { const ctx = use() const { syntax } = useTheme() + + const processedText = createMemo(() => { + const rawText = props.part.text.trim() + const sanitized = sanitizeTextForTerminal(rawText) + return processAnsiForTerminal(sanitized) + }) + return ( - + @@ -1020,7 +1034,11 @@ ToolRegistry.register({ name: "bash", container: "block", render(props) { - const output = createMemo(() => Bun.stripANSI(props.metadata.output?.trim() ?? "")) + const output = createMemo(() => { + const rawOutput = props.metadata.output?.trim() ?? "" + const sanitized = sanitizeTextForTerminal(rawOutput) + return processAnsiForTerminal(sanitized) + }) const { theme } = useTheme() return ( <> diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index 361e4525553..88473bfac3e 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -78,6 +78,21 @@ export namespace UI { } export function markdown(text: string): string { - return text + const { processAnsiForTerminal, sanitizeTextForTerminal, normalizePathForDisplay } = require("../util") + + // First normalize any paths in the markdown content for Git Bash compatibility + let processed = normalizePathForDisplay ? normalizePathForDisplay(text) : text + + // Sanitize the text for terminal compatibility + if (sanitizeTextForTerminal) { + processed = sanitizeTextForTerminal(processed) + } + + // Process ANSI sequences for the terminal + if (processAnsiForTerminal) { + processed = processAnsiForTerminal(processed) + } + + return processed } } diff --git a/packages/opencode/src/util/key-state-machine.ts b/packages/opencode/src/util/key-state-machine.ts new file mode 100644 index 00000000000..a032aa3847a --- /dev/null +++ b/packages/opencode/src/util/key-state-machine.ts @@ -0,0 +1,236 @@ +import { createStore } from "solid-js/store" +import { isDeepEqual } from "remeda" +import type { Keybind } from "./keybind" + +export type KeyState = + | "idle" // Normal key processing + | "leader" // Leader key pressed, waiting for sequence + | "chording" // Multiple keys held (chord detection) + | "sequencing" // Building key sequence after leader + | "timeout" // Leader mode expired + | "matched" // Complete sequence matched + | "failed" // No matching sequence found + +export interface KeyStateMachineContext { + state: KeyState + sequence: Keybind.Info[] + activeKeys: Set + leaderKey?: Keybind.Info + timeoutId?: NodeJS.Timeout + matchedKeybind?: string +} + +export interface KeyStateMachineEvent { + type: "KEY_DOWN" | "KEY_UP" | "TIMEOUT" + key: Keybind.Info + timestamp: number +} + +export class KeyStateMachine { + private store = createStore({ + state: "idle", + sequence: [], + activeKeys: new Set(), + }) + + private listeners: Array<(context: KeyStateMachineContext) => void> = [] + private keybinds: Record = {} + + constructor(keybinds: Record = {}) { + this.keybinds = keybinds + } + + getState(): KeyState { + return this.store.state + } + + getContext(): KeyStateMachineContext { + return { ...this.store } + } + + subscribe(listener: (context: KeyStateMachineContext) => void): () => void { + this.listeners.push(listener) + return () => { + const index = this.listeners.indexOf(listener) + if (index > -1) this.listeners.splice(index, 1) + } + } + + private notifyListeners() { + const context = this.getContext() + this.listeners.forEach(listener => listener(context)) + } + + private transition(newState: KeyState, updates: Partial = {}) { + this.store.setState(prev => ({ + ...prev, + state: newState, + ...updates, + })) + this.notifyListeners() + } + + private clearTimeout() { + if (this.store.timeoutId) { + clearTimeout(this.store.timeoutId) + this.store.setState("timeoutId", undefined) + } + } + + private startTimeout(ms: number = 2000) { + this.clearTimeout() + const timeoutId = setTimeout(() => { + this.handleEvent({ + type: "TIMEOUT", + key: { ctrl: false, meta: false, shift: false, leader: false, name: "" }, + timestamp: Date.now(), + }) + }, ms) + this.store.setState("timeoutId", timeoutId) + } + + private getKeySignature(key: Keybind.Info): string { + return `${key.ctrl ? "c" : ""}${key.meta ? "m" : ""}${key.shift ? "s" : ""}${key.name}` + } + + private matchKeybind(keybindName: string, key: Keybind.Info): boolean { + const keybind = this.keybinds[keybindName] + if (!keybind) return false + + return keybind.some(k => isDeepEqual(k, key)) + } + + private matchSequence(keybindName: string, sequence: Keybind.Info[]): boolean { + const keybind = this.keybinds[keybindName] + if (!keybind) return false + + // Check if sequence matches any keybind pattern + return keybind.some(k => { + if (k.leader && sequence.length > 0) { + // Check if first key is leader and rest matches + const [first, ...rest] = sequence + if (!first.leader) return false + + // Simple matching for now - can be enhanced + return rest.length === 1 && isDeepEqual(rest[0], { ...k, leader: false }) + } + return false + }) + } + + handleEvent(event: KeyStateMachineEvent) { + const { state, sequence, activeKeys, leaderKey } = this.store + + switch (state) { + case "idle": + if (event.type === "KEY_DOWN") { + if (this.matchKeybind("leader", event.key)) { + this.transition("leader", { + leaderKey: event.key, + sequence: [event.key], + }) + this.startTimeout() + } else if (activeKeys.size > 0) { + // Multiple keys pressed - potential chord + this.transition("chording") + } else { + // Single key - normal processing + this.transition("idle", { + sequence: [event.key], + }) + } + } + break + + case "leader": + if (event.type === "KEY_DOWN") { + this.clearTimeout() + const newSequence = [...sequence, event.key] + + if (this.matchSequence("any", newSequence)) { + this.transition("matched", { + sequence: newSequence, + matchedKeybind: "matched", + }) + } else { + this.transition("sequencing", { + sequence: newSequence, + }) + this.startTimeout() + } + } else if (event.type === "TIMEOUT") { + this.transition("timeout") + } + break + + case "chording": + if (event.type === "KEY_DOWN") { + // Add to active keys for chord detection + activeKeys.add(this.getKeySignature(event.key)) + } else if (event.type === "KEY_UP") { + activeKeys.delete(this.getKeySignature(event.key)) + + if (activeKeys.size === 0) { + // All keys released - check for chord match + if (sequence.length > 1) { + this.transition("matched", { + matchedKeybind: "chord", + }) + } else { + this.transition("failed") + } + } + } + break + + case "sequencing": + if (event.type === "KEY_DOWN") { + this.clearTimeout() + const newSequence = [...sequence, event.key] + + if (this.matchSequence("any", newSequence)) { + this.transition("matched", { + sequence: newSequence, + matchedKeybind: "matched", + }) + } else { + this.transition("sequencing", { + sequence: newSequence, + }) + this.startTimeout() + } + } else if (event.type === "TIMEOUT") { + this.transition("timeout") + } + break + + case "matched": + case "failed": + case "timeout": + // Reset to idle after a brief delay + setTimeout(() => { + this.transition("idle", { + sequence: [], + activeKeys: new Set(), + leaderKey: undefined, + matchedKeybind: undefined, + }) + }, 100) + break + } + } + + updateKeybinds(keybinds: Record) { + this.keybinds = keybinds + } + + reset() { + this.clearTimeout() + this.transition("idle", { + sequence: [], + activeKeys: new Set(), + leaderKey: undefined, + matchedKeybind: undefined, + }) + } +} \ No newline at end of file