feat: unified TUI editing with rules engine and typed widget values#306
Open
hangie wants to merge 54 commits intosirmalloc:mainfrom
Open
feat: unified TUI editing with rules engine and typed widget values#306hangie wants to merge 54 commits intosirmalloc:mainfrom
hangie wants to merge 54 commits intosirmalloc:mainfrom
Conversation
Implements conditional property override system for widgets (sirmalloc#38): - Rules execute top-to-bottom with optional stop flags - Numeric, string, boolean, and set operators - Cross-widget conditions (change one widget based on another's value) - Generic hide property for all widgets - TUI editors for rules and conditions - Renderer integration for rule-applied colors, bold, and hide Includes RulesEditor, ConditionEditor, ColorEditor TUI components, rules engine core with widget value extraction, and full test coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… operators Add missing string operators (equals, not equals, isEmpty, notEmpty) to the rules engine. Replace symbol-based operator labels (>, ≥, <, ≤, =, ≠) with readable text labels (greater than, equals, etc.) for better usability. Reorder operator picker to pair each operator with its negation. Fix condition editor displaying wrong label for boolean false conditions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires up the mode toggle infrastructure so Tab key switches between 'items' and 'color' modes in ItemsEditor, following the pattern established in RulesEditor. Uses widget's supportsColors() method to determine if Tab toggle is applicable for the selected widget. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…upport colors Prevents input misrouting by detecting at the top of the useInput handler that editorMode is 'color' but the currently selected widget doesn't support colors, and resetting back to 'items' mode. Also updates the Tab block comment to clarify it always consumes the Tab key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When editorMode is 'color', intercept input before widget picker/move mode handlers and delegate to the shared handleColorInput. Up/down arrows still navigate widgets, ESC cancels hex/ansi256 sub-modes or returns to items mode, and all other keys (left/right cycle colors, f/b/h/a/r) go through the shared color handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…olor mode Extract updateWidget callback before the ESC check and collapse the ESC branch so that sub-mode ESC falls through to handleColorInput rather than duplicating the inline closure. Add defensive comment on the unconditional return at the end of the color block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add visual feedback when in color mode: magenta mode indicator in title bar, current color info display with styled preview, hex/ANSI256 input prompts, styled widget labels using applyColors, and magenta selector arrow. Separators shown as dimmed in color mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build help text dynamically via buildHelpText() that switches between items mode and color mode keybinds. Color mode shows cycle/bold/reset hints plus hex/ansi256 conditionally on colorLevel. ESC now shows its destination in both modes. Rename "(x) exceptions" to "(x) rules". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the separate 'Edit Colors' entry point from MainMenu since color editing is now unified into the ItemsEditor via Tab toggle. Update menuSelections index references in App.tsx for terminalConfig (3→2) and globalOverrides (4→3) to account for the removed item. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The colorLines and colors AppScreen values became unreachable after Task 2.1 removed the 'Edit Colors' menu entry. This commit removes those screen states from the AppScreen type union, their JSX render blocks, and the ColorMenu import from the barrel import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ColorMenu and its array-based mutation helpers are now fully replaced by the shared handleColorInput handler in the unified editing flow. Removes ColorMenu.tsx, color-menu/mutations.ts, and its test file, and drops the ColorMenu export from the components barrel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tor module Create dedicated files for rule property input handling and condition/property formatting, extracted from RulesEditor.tsx for reuse by ItemsEditor's accordion mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add remaining rule-level input handlers to rules-editor/input-handlers.ts: - handleRuleColorInput: wraps shared handleColorInput with rule-aware onUpdate/onReset callbacks that route through extractWidgetOverrides - handleRuleMoveMode: swap rules on arrow keys, exit on Enter/ESC - addRule/deleteRule: CRUD with selection adjustment - handleRuleEditorComplete: custom editor completion routed through extractWidgetOverrides for rule.apply diff storage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the RulesEditor overlay with inline accordion state management. The x key now toggles expandedWidgetId (by widget id, survives reordering) and resets all rule-level state on expansion. Remove RulesEditor import and overlay rendering block. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route keyboard input to rule-level handlers when expandedWidgetId is set. Handles condition editor passthrough, rule move mode, rule color mode with ESC peeling, and rule property mode with add/delete/condition editor keys. Updated handleEditorComplete to route through handleRuleEditorComplete when rules are expanded. No fallthrough to widget-level handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Render rules inline below their parent widget when expanded via 'x' key. Each rule line shows indented selector, styled label with rule colors, condition summary, stop indicator, and applied properties. Parent widget loses selector arrow and (N rules) annotation when expanded. Selector colors match mode: green for property, magenta for color, blue for move. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…play
- buildHelpText() now checks expandedWidgetId first and returns context-appropriate
help for rule move mode, rule color mode, and rule property mode (with widget
custom keybinds, raw value, and merge hints)
- Title bar shows "Edit Line N — Rules for {widgetDisplayName}" when rules are
expanded; mode indicators (MOVE/COLOR) respect whether we are in rule-level or
widget-level context
- Rule-level color info display shows current foreground/background using
mergeWidgetWithRuleApply(baseWidget, rule.apply) as the temp widget
- Hex/ANSI256 input prompts rendered at rule level using ruleColorEditorState
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ConditionEditor import and render it as an early-return overlay when ruleConditionEditorIndex is set and an expanded widget exists. The onSave callback updates the rule's when condition in the full widgets array and calls onUpdate, then clears the editor index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced by accordion rules editor inline in ItemsEditor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Normal mode: → expands rules for selected widget, ← collapses if expanded. Move mode: ←→ opens the type picker. The `x` key no longer triggers rules expansion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In items mode, replace '←→ open type picker' with '→ expand rules' and remove '(x) rules' since → now handles rule expansion directly. In move mode, add '←→ change type' alongside the existing move instructions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an accordion is expanded (→ pressed) on a widget with zero rules, display an indented dimmed message '(no rules — press 'a' to add)' instead of rendering nothing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve mode Move ←→ condition editor opening from rule property mode to rule move mode (focus mode), mirroring the widget-level pattern. In property mode, ← now collapses rules and → is a no-op. In move mode, ←→ opens the condition editor and exits move mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove stale ←→ edit condition from rule property mode (where left arrow now collapses) and add it to rule move mode where the keybind actually lives. Also add ← collapse hint to rule property mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Right arrow drills into a line (same as Enter/select), left arrow goes back (same as ESC). Both are disabled during move mode and delete confirmation dialogs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When at the widget level in items mode with no rules expanded, pressing ← now navigates back to LineSelector (same behavior as ESC). The existing collapse behavior (← when rules are expanded) is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Default wrapNavigation to true so ↑↓ wraps around the list - Add onBack prop (optional); ← fires onBack when provided - Add → as alias for Enter to trigger onSelect on the selected item Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…List
The List component now handles ← (fires onBack) and → (fires onSelect)
natively. Remove the duplicate leftArrow/rightArrow handlers from
LineSelector's useInput and pass onBack={onBack} to the List so the
built-in navigation takes over. ESC still calls onBack via useInput for
the non-List cases (move mode, theme-managed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass onBack to the List component in PowerlineSetup, TerminalOptionsMenu, TerminalWidthMenu, InstallMenu, and PowerlineThemeSelector so that ← exits these screens, matching existing ESC behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…neSeparatorEditor Adds key.leftArrow alongside key.escape in both non-List screens so users can press ← to navigate back. In edit/hex-input modes, ← cancels the edit (matching ESC). In PowerlineSeparatorEditor normal mode, ← is already used for cycling presets so only hex-input-mode cancel is added there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With ← arrow key now handling back navigation via List's onBack prop, the "← Back" menu items are no longer needed. Remove showBackButton prop from List, the useMemo that appended back items, and all dead 'back' value checks in onSelect/onSelectionChange handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…operty mode → - Rule property mode: → now opens condition editor (was a silent no-op) - Rule move mode: removed ←→ handler; ↑↓ reorder and Enter/ESC exit only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In property mode, show '→ edit condition' so users know how to access condition editing. In move mode, remove the stale '←→ edit condition' reference since that shortcut no longer applies there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…diting Free ← for back navigation by moving preset cycling and toggle-invert into a focus mode entered via Enter. Normal mode now uses wrapping ↑↓ navigation and ← for back. Focus mode provides ←→ preset cycling, ↑↓ reorder, and (t)oggle invert for separators. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `const cmd = input.toLowerCase()` at the start of keybind sections in GlobalOverridesMenu, PowerlineSetup, PowerlineSeparatorEditor, and PowerlineThemeSelector. Replace all `input === 'x' || input === 'X'` patterns with `cmd === 'x'`. Text input modes (hex digits, padding, separator text) continue to use the original `input` to preserve case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Makes focus mode more discoverable by accurately describing that Enter enters edit mode (which supports type changes and more, not just moving). Move mode help text is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create reusable parse helpers for converting widget raw output to typed values. These pure functions will be used by widgets in their getValue() implementations. Parsers added: - parsePercentage: strips % suffix and returns number - parseCurrency: strips currency symbols and returns number - parseTokenCount: handles K/M/B suffixes (case-insensitive) - parseSpeed: strips t/s suffix and returns number - parseBooleanString: parses true/false strings (case-insensitive) - parseIntSafe: safely parses integer strings, rejects floats All parsers include strict validation and comprehensive test coverage (21 tests, 73 assertions).
…t interface Extends the Widget interface to support typed value extraction for rule evaluation: - Add getValueType() to declare the value type (string, number, or boolean) - Add getValue() to extract the typed value for rule evaluation - Remove getNumericValue() (replaced by getValue) The return type of getValue() must align with what getValueType() declares: - If getValueType() returns 'number', getValue() must return number | null - If getValueType() returns 'string', getValue() must return string | null - If getValueType() returns 'boolean', getValue() must return boolean | null This is a convention enforced by documentation, not TypeScript (since the return type is a union). Updated tests to cover rules with numeric, string, and boolean comparisons. Expected type errors in src/utils/widget-values.ts will be fixed in Phase 3.
Implement getValueType() and getValue() on 7 number widgets: - ContextPercentage, ContextPercentageUsable (use parsePercentage) - ContextLength (uses parseTokenCount) - TokensInput, TokensOutput, TokensCached, TokensTotal (use parseTokenCount) All widgets follow the pattern: - getValueType() returns 'number' - getValue() calls render() with rawValue: true and parses result Add comprehensive test coverage: - getValue tests for all 7 widgets - Tests cover live data, preview mode, and null handling - Tests verify percentage parsing for context widgets - Tests verify token count parsing with K/M suffixes - New ContextLength.test.ts file created All 365 widget tests passing.
The gitData field was accidentally re-added during the widget getValue implementation. It is unused dead code — git widgets use the module-level cache in git.ts, not context-level data.
Add typed value extraction to SessionCost, TerminalWidth, and the three speed widgets (InputSpeed, OutputSpeed, TotalSpeed). All widgets now declare getValueType(): 'number' and implement getValue() using appropriate parsers: - SessionCost uses parseCurrency to strip currency symbols - Speed widgets use parseSpeed to strip 't/s' suffix - TerminalWidth uses parseIntSafe for plain integer parsing Tests cover live data, preview mode, and missing data cases for all widgets.
- Add getValueType() returning 'number' to both widgets - Implement getValue() to extract percentage from context.usageData - Return hardcoded preview values (20 for session, 12 for weekly) in preview mode - Handle null/error cases by returning null - Clamp percentages to 0-100 range - Extract directly from context data, not from rendered output - Add comprehensive test coverage for getValue() method
…GitUntracked, GitIsFork, GitWorktreeMode, GitChanges) Add getValueType() returning 'boolean' and getValue() to all six boolean widgets. Remove getNumericValue() from GitStaged, GitUnstaged, and GitUntracked. GitChanges uses direct git data check instead of render since it doesn't support rawValue.
…rom GitAheadBehind
- Add getValueType() and getValue() to GitConflicts widget
- Returns numeric conflict count via parseIntSafe
- Replaces deprecated getNumericValue() method
- Remove getNumericValue() from GitAheadBehind widget
- Widget's raw value is a compound pair ('2,3') with no single numeric interpretation
- Previous sum behavior (ahead + behind) was semantically incorrect
- Widget will now work with string operators via fallback
- Update widget-values.ts to support new getValue pattern
- Checks for getValue/getValueType instead of deprecated getNumericValue
- Maintains backward compatibility during transition
- Add comprehensive tests for GitConflicts.getValue()
- Tests conflict count extraction, preview mode, and no-git-repo cases
Replace guess-based value extraction with dispatch on widget's declared value type. getWidgetValue() now calls widget.getValue() when available, falling back to rendering in raw mode for widgets without getValue(). Remove parseNumericValue, supportsNumericValue, getWidgetNumericValue, getWidgetStringValue, getWidgetBooleanValue, and DEBUG_RULES logging. Rewrite tests to cover typed dispatch and string fallback paths.
Update cross-widget tests to use isTrue operator instead of greaterThan for git-changes (now a boolean widget). Add test coverage for: - git-staged boolean widget with isTrue - Boolean coercion from numeric values (non-zero = true, zero = false) - isFalse on boolean widgets - Cross-widget conditions with typed boolean and numeric values
The comments on total_input_tokens and used_percentage were ambiguous about which widget each value corresponds to. Clarified that 9.3% is the total context percentage and 11.6% is the usable context percentage at 80% usable threshold.
The gitData field on RenderContext was removed as dead code earlier, but was re-introduced in StatusLinePreview.tsx during widget getValue implementation. Remove the references again since the field no longer exists on RenderContext.
When a widget declares a value type (number, boolean, string), the operator picker now only shows relevant operator categories: - Number widgets: Numeric, Set - String widgets: String, Set - Boolean widgets: Boolean only - Untyped widgets: all categories (unchanged) When the user changes the target widget, the current operator is automatically reset if it's incompatible with the new widget's type.
…mocks Two issues that caused CI failures: 1. Duplicate imports: subagent added a separate import of DEFAULT_SETTINGS from '../types/Settings' instead of merging into the existing Settings type import. This caused eslint-plugin-import to crash on the duplicate import path. Merged into single import across 18 widget files. 2. Partial vi.mock: GitIsFork.test.ts and widget-values.test.ts replaced the entire git-remote module with only getForkStatus, leaving listRemotes undefined. This leaked across test files in the same Vitest worker, causing git-remote.test.ts failures in CI. Replaced with vi.spyOn.
ESLint auto-fix for issues introduced by subagents: - Multi-line imports where >1 elements (import-newlines/enforce) - Newline before return in single-line if blocks (nonblock-statement-body-position) - Trailing newlines at end of files (eol-last) - Unnecessary type arguments in ConditionEditor (no-unnecessary-type-arguments) - Object curly newline formatting in test files
Contributor
Author
|
Implements #38 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
number,boolean, or string fallback) so the rules engine evaluates conditions using the widget's own typed value extraction instead of fragile render-and-parse heuristicsDetails
Rules Engine
stopflag>,>=,<,<=,=,≠(numeric);contains,startsWith,endsWith,equals,isEmpty(string);isTrue,isFalse(boolean);in,notIn(set)Typed Widget Values
value-parsers.ts) for consistent value extractiongetNumericValuefrom Widget interface, replaced withgetValueType()/getValue()parseNumericValueheuristics andsupportsNumericValue— widgets own their own parsingTUI Improvements
Test plan
🤖 Generated with Claude Code