Skip to content

Commit 87f7b79

Browse files
joshsmithxrmclaude
andauthored
fix(tui): polish sprint - cursor, hotkeys, auth flow, menus (#407)
* fix(tui): improve cursor visibility in text fields Add high-contrast TextInput color scheme to TuiColorPalette with BrightCyan background on focus for better cursor visibility. Apply this scheme to all TextField instances across dialogs and screens. Files updated: - TuiColorPalette.cs: Add TextInput color scheme - ProfileCreationDialog.cs: 7 TextFields - EnvironmentSelectorDialog.cs: 2 TextFields - ClearAllProfilesDialog.cs: 1 TextField - QueryHistoryDialog.cs: 1 TextField - SqlQueryScreen.cs: 1 TextField - DataTableView.cs: 1 TextField - ProfileSelectorDialog.cs: 1 TextField (rename dialog) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): replace auth method combobox with radio group Replace ComboBox with RadioGroup in ProfileCreationDialog for reliable rendering. ComboBox had known z-order and redraw issues in Terminal.Gui 1.x that caused visual artifacts. Changes: - Replace _authMethodCombo with _authMethodRadio (RadioGroup) - Adjust dialog height from 24 to 27 to accommodate RadioGroup - Adjust Y positions of subsequent elements - Update event handler signature to SelectedItemChangedArgs Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): show environment selector after auth success Replace success message with immediate environment selector dialog after authentication completes. This provides a seamless flow where users authenticate, pick their environment, and the profile is ready. Changes to ProfileCreationDialog: - Remove success MessageBox - Show EnvironmentSelectorDialog immediately after auth - Add SelectedEnvironmentUrl/Name properties for post-auth selection Changes to MainWindow: - Add SetActiveProfileWithEnvironmentAsync method - Use post-auth selected environment if available Flow: Auth → Environment Selection → Close (no success message) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): document menu keyboard shortcuts Update help dialog to document the menu navigation pattern: - Alt+Letter opens menus globally (Alt+F, Alt+E, Alt+T, Alt+H) - Single letter selects item when menu is open (underscore convention) - Arrow keys, Enter, and Esc for menu navigation Terminal.Gui handles single-letter hotkeys when menus are open via the underscore prefix convention (_File, _SQL Query, etc.) which is already in place. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): add menu click debounce to prevent flicker Add 150ms debounce on MenuBar mouse click events to prevent the intermittent double-fire issue where menus immediately close after opening. Terminal.Gui 1.x can fire multiple events for a single click in some terminal emulators. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): allow deletion of active/last profile Previously the TUI blocked deletion of the active profile with "Switch to a different profile first." This made it impossible to delete the last remaining profile. The app already handles null authentication gracefully - TUI starts with no profile, CLI commands fail with helpful messages. Now the TUI allows deleting any profile with appropriate warning if it's active. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use block cursor for text field visibility Replace high-contrast BrightCyan background workaround with proper block cursor via ANSI escape sequence (DECSCUSR). This addresses the root cause - thin cursor visibility - rather than masking it with jarring background colors. - Add \x1b[2 q (steady block cursor) before Application.Init() - Restore default cursor \x1b[0 q on shutdown - Tone down TextInput focus from BrightCyan to Cyan Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): remove text field highlight and fix cursor position - Remove cyan background from TextInput focus state - block cursor provides sufficient visibility without jarring color changes - Add SetFocus() to ProfileCreationDialog to position cursor in name field on load instead of top-left corner Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): defer SetFocus to Ready event for cursor positioning Move SetFocus() call from constructor to Ready event handler to ensure layout is complete before positioning cursor. Constructor-time focus was causing cursor to appear at dialog origin instead of text field. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): add keyboard handlers to dialogs Add Enter and Escape key handlers to improve keyboard UX: - ProfileCreationDialog: Enter on auth method RadioGroup advances to URL field, Escape closes dialog (if not authenticating) - ExportDialog: Enter on format RadioGroup triggers export, Escape closes - EnvironmentSelectorDialog: Enter on URL field triggers select, Escape closes - ProfileSelectorDialog: Escape closes dialog (added to existing OnKeyPress) Terminal.Gui RadioGroup uses Space for selection by default; Enter was not handled, breaking user expectations. These handlers make dialogs more keyboard-friendly. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use GetByNameOrIndex for profile lookup Profile lookup was using GetByName which fails when profile has no name and DisplayIdentifier returns "[1]" format. Changed to GetByNameOrIndex which handles both named profiles and index references. Fixed in: - InteractiveSession.InitializeAsync - for TUI session initialization - ProfileServiceFactory.CreateFromProfileAsync - for service provider creation - DaemonConnectionPoolManager - for daemon connection pool creation This fixes "Profile '[1]' not found" error in Environment Details dialog when using unnamed profiles. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent CTS disposed error in EnvironmentDetailsDialog Add _disposed flag to track dialog disposal state and check it before accessing cancellation token in MainLoop.Invoke callbacks. The issue occurred because: 1. Dialog closes, triggering Dispose() 2. Dispose() cancels and disposes CancellationTokenSource 3. Async callback still running, tries to check token.IsCancellationRequested 4. Accessing disposed CTS token throws ObjectDisposedException Now the callbacks check _disposed first, avoiding the token access after CTS disposal. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): remove Enter handler from auth RadioGroup The Enter handler was causing a flicker without providing useful behavior. RadioGroup selection happens via arrow keys which select immediately as you navigate. Removed the handler to restore default Terminal.Gui behavior. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): handle bracket notation in GetByNameOrIndex GetByNameOrIndex("[1]") was failing for unnamed profiles because int.TryParse("[1]") returns false. The TUI uses DisplayIdentifier (which returns "[N]" for unnamed profiles) when deleting/renaming, causing these operations to silently fail. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): make Enter select RadioGroup items Terminal.Gui RadioGroup only binds Space to selection by default. Arrow keys move the cursor but don't change SelectedItem until Space is pressed. Added Enter key handler that simulates Space to match user expectations. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add AboutDialog with official branding - Create dedicated AboutDialog replacing MessageBox - Apply official tagline from branding guidelines - Add docs site link alongside GitHub repo - Include copyright notice Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): adjust AboutDialog layout to prevent overlap - Widen dialog to fit full GitHub URL - Increase height for proper button spacing - Move copyright above close button Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use semantic version and fix URL truncation in AboutDialog - Use ErrorOutput.Version for full semantic version with beta suffix - Move URLs to separate lines to prevent truncation - Adjust dialog dimensions for new layout Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): increase AboutDialog height for button spacing Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): consolidate TUI entry points and fix device code dialog - Remove redundant Discover button from ProfileCreationDialog - Fix device code rendering: use MessageBox instead of nested dialog - Consolidate ppds and ppds interactive to use same LaunchTui method - Delete unused DeviceCodeAuthDialog.cs - Auto-copy device code to clipboard for convenience Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): improve error dialog readability and clean up menu labels Add ReadOnlyText color scheme to prevent cyan background on focused read-only TextViews (was making error details unreadable). Remove trailing ellipsis from menu item labels for cleaner appearance. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): resolve hotkey conflicts and improve status bar readability - Fix duplicate Alt+C hotkey: change Close to Clos_e (Alt+E) - Fix duplicate Alt+L hotkey: change C_lear All to Clear _All (Alt+A) - Improve StatusBar_Default contrast: black on gray for readability Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): remove trailing slash from docs URL for consistency Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add interactive status bar with profile/environment switching - Create TuiStatusBar component with clickable profile/environment sections - Show environment-aware coloring (red=Production, yellow=Sandbox, green=Dev) - Remove Environment menu - switching now via status bar clicks - Simplify File menu - profile actions moved to ProfileSelectorDialog - Add Details and Clear All buttons to ProfileSelectorDialog (two-row layout) - Add Details button to EnvironmentSelectorDialog - Add CurrentProfileName property and ProfileChanged event to InteractiveSession - Update SqlQueryScreen to use shared TuiStatusBar component Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): polish profile selector dialog and status bar display - Show identity (email/app ID) in status bar profile display - Remove grey background from hint label in profile selector - Remove asterisk from profile list (confusing with session-only switching) Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add context-aware HotkeyRegistry for global shortcuts - Add HotkeyRegistry with Global/Screen/Dialog scope layers - Alt+P and Alt+E now work everywhere (close dialogs first) - Screen hotkeys (Ctrl+E, Ctrl+H) blocked when dialog is open - Fix button label conflicts (_Export → E_xport, etc.) - Update F1 help with new shortcuts Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): remove pool warming on startup to prevent login prompt TUI was triggering auth dialog immediately on startup due to pool warming in InitializeAsync(). This was introduced in cb41e2e to improve first-query latency, but became problematic after 6051698 added PreAuthenticationDialog which made the auth visible as a dialog. Changes: - Remove WarmPoolAsync() call from InitializeAsync() - Delete unused WarmPoolAsync() method - Connection/auth now happens lazily on first query This returns to the pre-cb41e2e behavior where TUI starts instantly without prompting for authentication. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): always show profile index in DisplayIdentifier Change DisplayIdentifier to always include the index prefix: - Named profile: "[1] MyProfile" (was just "MyProfile") - Unnamed profile: "[1]" (unchanged) This makes profile identification consistent across CLI and TUI, allowing users to always reference profiles by index. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent double key processing causing F1+Escape crash - Remove duplicate shortcut: params from F1/F2/F12 menu items (already registered as global hotkeys in HotkeyRegistry) - Add using pattern to all dialog creation methods for proper disposal - Clear active dialog state immediately when handling global hotkeys The crash occurred because F1/F2/F12 were registered in both the menu system AND HotkeyRegistry, causing Terminal.Gui to process these keys through multiple paths and corrupt internal state. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent nested SqlQueryScreen creation on repeated F2 F2 global hotkey was creating stacked Application.Run() calls when pressed repeatedly or from within SqlQueryScreen. This corrupted Terminal.Gui internal state and caused Border.SetBorderBrush crash. - Add ActiveScreen property to IHotkeyRegistry - Check if already in SqlQueryScreen before creating new one Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use custom dialog for keyboard shortcuts instead of MessageBox Terminal.Gui's MessageBox.Query has a bug where Border.SetBorderBrush crashes with null reference after rapid screen/dialog transitions. Replace with custom KeyboardShortcutsDialog that we control. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): defer global hotkey handlers to prevent Terminal.Gui state corruption Root cause: Starting Application.Run() from within a key event handler (HotkeyRegistry.TryHandle) corrupts Terminal.Gui's internal state, causing Border.SetBorderBrush to crash with NullReferenceException. The fix defers ALL global hotkey handlers to the next main loop iteration via MainLoop.Invoke(). This ensures the current key event fully completes before any new Application.Run() calls start. Also adds proper disposal to SqlQueryScreen. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent duplicate global hotkey handlers from stacking up When F1/F2 keys are pressed rapidly, multiple handlers were queued via MainLoop.Invoke(). These would all execute sequentially, creating overlapping dialogs/screens that corrupt Terminal.Gui state. Fix: Track pending global handlers in a HashSet. Ignore subsequent key presses if a handler for that key is already pending. Clear the pending state after handler completes. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent cross-key global hotkey race condition causing crash The per-key HashSet only prevented same-key duplicates (F1+F1) but not cross-key stacking (F1+F2). When F2 was pressed while F1's dialog was open, RequestStop() was called but the new Application.Run() started before cleanup finished, corrupting Terminal.Gui's ConsoleDriver state. Solution: Replace HashSet<Key> with single boolean gate. When a global hotkey is pressed while dialog is open, just close the dialog - don't execute the handler. User must press key again after dialog closes. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor(tui): add consistent 2-line footer across all screens - Add TuiStatusLine component for contextual status messages and spinner - Remove status message functionality from TuiStatusBar (now profile/env only) - Update MainWindow and SqlQueryScreen to use new 2-line footer pattern - Profile/environment bar stays at bottom, status line above it - Spinner no longer hides profile/env buttons during operations Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add autonomous testing feedback loop with state capture and tui-test Enable Claude to autonomously iterate on TUI code without human visual inspection. Phase 1 - State Capture Infrastructure: - Add ITuiStateCapture<T> interface for testable state exposure - Add 15 state record types for screens, dialogs, and components - Implement CaptureState() on MainWindow, TuiStatusBar, SqlQueryScreen, ProfileSelectorDialog, and ErrorDetailsDialog - Fix 2 failing TuiUnit tests (lazy-loading behavior after commit 9a14526) Phase 2 - Visual Snapshot Testing: - Add tui-e2e npm project using @microsoft/tui-test - Add startup flow tests with snapshot verification - Add CI workflow for TUI E2E tests on TUI file changes - Update /test command to include tui-e2e in TUI testing flow Claude's new feedback loop: - Fast logic: dotnet test --filter "Category=TuiUnit" - Visual: npm test --prefix tests/tui-e2e Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore(tui-e2e): pin Node 20 LTS and add setup docs - Add .nvmrc pinning Node 20 (required by @microsoft/tui-test) - Add README with setup instructions and usage - Fix package.json version (0.0.1-rc.5 is latest available) Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs(tui-e2e): add nvm-windows installation instructions Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui-e2e): configure @microsoft/tui-test correctly for ESM - Add ESM support: "type": "module" in package.json, ESNext in tsconfig - Switch test runner from Playwright to @microsoft/tui-test CLI - Use test.use({ program }) API instead of terminal.spawn() - Fix path resolution using process.cwd() for correct repo root - Add tui-test.config.ts with trace enabled - Add initial snapshot for main menu visual regression - Update .gitignore for tui-traces and .tui-test cache Tests now pass: launches main menu, status bar, Ctrl+Q quit Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore(tui-e2e): remove unused playwright.config.ts Now using tui-test.config.ts with @microsoft/tui-test runner. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(commands): add /tui-review for UX evaluation Enables structured TUI screen evaluation with: - Setup and launch instructions - Interaction checklist - Structured report format (What works, Issues, Improvements) - Severity levels (P1/P2/P3) - Screen file location reference Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): resolve SQL Query screen UX issues - Fix ClearData() call on environment switch (was Clear() which only cleared visually) - Change query history hotkey to Ctrl+Shift+H (Ctrl+H now exclusively for GUID toggle) - Remove status bar buttons from Tab order with CanFocus=false - Add F6 hotkey to toggle focus between query input and results table - Fix Escape to return to query input when in results (not close screen) - Fix filter frame layout by adjusting results table Y position on show/hide - Add visual focus indicators using TuiColorPalette.Focused on Enter/Leave - Update query frame title hint to include F6 shortcut Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(tui-e2e): add SQL Query screen E2E tests Add automated tests for SQL Query screen using @microsoft/tui-test: - F2 opens SQL Query screen - Default query pre-populated - Escape returns to main menu - Ctrl+E shows error when no data - Ctrl+Shift+H behavior without environment - Status bar displays profile info - Tab navigation between controls - Visual regression snapshots 12 tests now pass covering startup and SQL Query flows. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): resolve UX review issues round 2 P2 fixes: - Fix status line not rendering during construction (MainLoop null) - Add text truncation with ellipsis to status bar buttons P3 fixes: - Add ▶ prefix to focused frame titles for better visibility - Center "No data" message in results table Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(tui-e2e): update snapshots for status bar and focus fixes Updated snapshots reflect UX improvements: - Status bar now truncates profile with ellipsis, shows full env name - Focus indicator (▶) now appears in focused frame title Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): properly fix status line and No data centering Status line fix: - Use Label child view instead of manual Driver drawing - Driver/Bounds are null during construction, Label handles rendering No data centering fix: - Use TextAlignment.Centered instead of Pos.Center() - Pos.Center() with Dim.Fill() doesn't center text content Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(tui-e2e): update snapshots for status line and centered empty state All 4 UX issues now verified fixed: - Status line renders ("Connecting to environment...") - "No data" text centered in Results frame - Status bar truncates profile, shows full environment - Focus indicator (▶) visible in active frame Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): set environment URL synchronously to fix status bar connection bug The status bar showed the environment correctly but SQL queries failed with "No environment selected" because _currentEnvironmentUrl was never set in InitializeAsync(), SetEnvironmentAsync(), and SetActiveProfileAsync(). - InitializeAsync: now sets _currentEnvironmentUrl along with display name - SetEnvironmentAsync: now sets _currentEnvironmentUrl after invalidation - SetActiveProfileAsync: now sets URL and fires EnvironmentChanged synchronously instead of inside async continuation (matching SetEnvironmentAsync behavior) Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor(tui): use hotkey registry for Ctrl+Enter in SQL query screen Replace direct KeyPress handler with registered hotkey for consistency with other keyboard shortcuts in the application. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): restore status bar click handling with custom MouseEvent Terminal.Gui buttons with CanFocus=false don't receive click events. Added MouseEvent override to handle clicks at the View level, preserving clean tab order (query ↔ results only) while restoring mouse interaction. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): rewrite status bar with direct rendering for reliable mouse handling Terminal.Gui child views consume mouse events before parents can handle them, even with CanFocus=false. Rewrote TuiStatusBar to: - Remove all child views (Labels/Buttons) - Draw content directly in Redraw() override - Set WantMousePositionReports=true to receive mouse events - Handle clicks in MouseEvent without child view interference Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor(tui): implement shell architecture with persistent menu/status bar Replace MainWindow with TuiShell that provides: - Persistent menu bar visible on all screens - Persistent status bar (no duplication) - Content area where screens swap via NavigateTo/NavigateBack - Context-aware menus (screen-specific items inserted between File and Help) Refactor SqlQueryScreen to implement ITuiScreen interface: - No longer inherits from Window - Provides Content view, Title, and ScreenMenuItems - Uses CloseRequested event for navigation instead of RequestStop - Hotkeys registered in OnActivated, unregistered in OnDeactivating This fixes the issue where menu bar was not visible on SqlQueryScreen and eliminates duplicated status bar code. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): polish UI colors and column sizing - Fix results table highlight: only change title, not entire table color - Auto-size columns based on content with padding and breathing room - Fix query history dialog: proper color schemes for ListView and TextView - Remove Ctrl+Q quit shortcut (conflicts with Query menu, Alt+F4 is standard) - Fix export dialog: dark background instead of grey - Dark menu bar with cyan accents to match overall theme Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): fix filter functionality and add status auto-restore Filter: - Filter now actually filters visible rows (was only updating DefaultView) - Creates filtered DataTable from DefaultView.ToTable() and assigns to TableView - Stores unfiltered data for restore when filter is cleared - Shows "(filtered: X of Y)" in persistent status line Status line: - Temporary messages (copy, errors) auto-restore after 2.5s - Uses token-based cancellation to handle rapid successive messages - Filter info persists in main status, not as temporary message Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): apply color scheme recursively to SaveDialog subviews The SaveDialog's internal file list wasn't inheriting the color scheme. Now recursively applies TuiColorPalette.Default to all subviews. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add JSON export, fix profile state, improve UX - Fix profile state bug: update _profileName in InitializeAsync when using active profile so CurrentProfileName returns actual name - Fix SaveDialog colors: apply color scheme in Loaded event to prevent Terminal.Gui's default blue background from leaking through - Add Ctrl+W and Q shortcuts for direct window close (hybrid escape UX) - Add JSON export with type preservation using column metadata JSON export converts string values back to proper types (guid, int, decimal, boolean, datetime) using the QueryColumnType metadata stored during query result conversion. Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs: document TUI testing philosophy and workflow Codify the principle that CLI tests verify service logic while TUI tests verify presentation. This avoids duplicating test coverage across layers. Changes: - CLAUDE.md: Add NEVER rule for re-testing services in TUI E2E - ADR-0028: Add Test Responsibility Matrix and development workflow - /test command: Add TUI philosophy and snapshot diff guidance Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add TuiDialog base class for standardized dialog behavior Adds TuiDialog abstract base class that all dialogs should inherit from. Features: - Automatic HotkeyRegistry integration (blocks screen-scope hotkeys while dialog is open) - Consistent ColorScheme (TuiColorPalette.Default) - Standard Escape key handling (override OnEscapePressed for custom behavior) - Proper cleanup on disposal This addresses the issue where only 1 of 12 dialogs properly registered with the HotkeyRegistry, causing screen-scope hotkeys like F2 to fire while dialogs were open. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): add Application.Refresh() before PreAuthenticationDialog to fix border rendering When PreAuthenticationDialog is shown from a MainLoop.Invoke() callback triggered by background authentication, Terminal.Gui's ConsoleDriver state may be unstable. This caused the dialog content to render but the border/frame to be missing. Adding Application.Refresh() before showing the dialog forces a full screen redraw, stabilizing the ConsoleDriver state before Application.Run() starts the nested event loop. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor(tui): migrate all dialogs to TuiDialog base class Migrates all 12 dialogs to inherit from TuiDialog, providing: - Automatic HotkeyRegistry integration (SetActiveDialog/clear on dispose) - Consistent ColorScheme (TuiColorPalette.Default) - Standard Escape key handling (override OnEscapePressed for custom behavior) - Proper cleanup on disposal Dialogs migrated: - AboutDialog, ClearAllProfilesDialog, EnvironmentDetailsDialog - EnvironmentSelectorDialog, ErrorDetailsDialog, ExportDialog - KeyboardShortcutsDialog, PreAuthenticationDialog, ProfileCreationDialog - ProfileDetailsDialog, ProfileSelectorDialog, QueryHistoryDialog Special cases: - PreAuthenticationDialog: Override OnEscapePressed to set Result before close - ProfileCreationDialog: Override OnEscapePressed to block during auth - EnvironmentDetailsDialog: Keeps existing Dispose for CancellationTokenSource This fixes the issue where only 1 of 12 dialogs properly registered with the HotkeyRegistry, causing screen-scope hotkeys like F2 to fire while dialogs were open. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): remove global menu item hotkeys, keep menu bar hotkeys only Terminal.Gui's MenuItem underscore convention (_Quit) creates global Alt+letter hotkeys that work even when menus aren't open. This caused Alt+Q to quit the app from anywhere, conflicting with the Query menu. Solution: - Keep underscores on MenuBarItem names (_File, _Tools, _Help, _Query) so Alt+F/T/H/Q open the respective menus - Remove underscores from MenuItem names (Quit, About, Execute, etc.) so they no longer create global hotkeys Result: - Alt+F opens File menu, Alt+Q opens Query menu (when on SQL screen) - Once menu is open, first-letter navigation still works (Q for Quit) - No more accidental Alt+Q closing the app Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): implement ITuiStateCapture on all dialogs Adds ITuiStateCapture implementation to all 12 dialogs, enabling autonomous testing via state capture pattern. Tests can now verify dialog state without requiring Terminal.Gui runtime. Dialogs with state capture: - AboutDialog: ProductName, Version, Description, GitHubUrl - ClearAllProfilesDialog: WarningMessage, ProfileCount, button state - EnvironmentDetailsDialog: DisplayName, Url, Type, OrgId, Version - EnvironmentSelectorDialog: Environment list, selection, loading state - ErrorDetailsDialog: Error list, selected error, details - ExportDialog: Format selection, row count, headers option - KeyboardShortcutsDialog: Shortcut list with key/description/scope - PreAuthenticationDialog: Available options, selected option - ProfileCreationDialog: Auth method, profile name, creation state - ProfileDetailsDialog: Profile info, identity, environment details - ProfileSelectorDialog: Profile list, selection, loading/error state - QueryHistoryDialog: History items, selection, empty state Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(tui): add dialog state capture unit tests Adds unit tests demonstrating the ITuiStateCapture pattern for dialogs. Tests can verify dialog state without Terminal.Gui runtime, enabling autonomous testing in CI/CD pipelines. Test coverage includes: - AboutDialog: Title, ProductName, Version, GitHubUrl - KeyboardShortcutsDialog: Shortcuts list with global and screen-specific - PreAuthenticationDialog: Available options, device code presence - State record equality verification Total test count: 35 → 46 (+11 dialog tests) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): fix spinner rendering and improve status bar layout TuiSpinner: - Fix rendering by using proper Terminal.Gui pattern (Redraw override) - Was incorrectly trying to draw directly in Invoke() callback - Now stores display text and lets Terminal.Gui call Redraw() TuiStatusBar: - Pin profile to left, environment to right (better use of space) - Remove hard-coded truncation limits (18/28 chars) - Dynamic truncation based on actual terminal width SqlQueryScreen: - Add spinner + status label for query execution feedback - Shows "Executing query..." during execution Dialogs: - Defer spinner start to Loaded event (ensures dialog is visible) - EnvironmentDetailsDialog: responsive width (Dim.Percent(80)) F2 key conflict: - Remove F2 global shortcut for SQL Query (was conflicting with rename) - F-keys now reserved for actions, menus for navigation Other: - Remove Quit button from main menu (use File > Quit or Ctrl+Q) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): persist HomeAccountId after environment discovery auth Environment Selector was prompting for login every time because GlobalDiscoveryService did not expose the HomeAccountId captured during interactive authentication. EnvironmentService now persists this value to the profile, enabling silent token acquisition on subsequent discovery calls. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): polish punch list - cursor, rename, dropdown, focus - Set cursor color to black via OSC 12 escape (terminal-dependent) - Fix profile rename error: use DisplayIdentifier instead of Name - Add FileDialog color scheme with white text on cyan for readability - Auto-focus filename field when SaveDialog opens Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): set Colors.Menu for ComboBox dropdown readability Terminal.Gui ComboBox popups use the global Colors.Menu scheme, not the parent view's ColorScheme. Now setting Colors.Menu to FileDialog scheme before showing SaveDialog, then restoring after. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add Ctrl+A select all in SQL query editor Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): fix Ctrl+A/C/X keyboard shortcuts in SQL query editor - Ctrl+A: Use ProcessKey to properly select all text (SelectAll() just moves cursor) - Ctrl+C/X: Add HasFocus guard to prevent table handler firing when query input focused Fixes issue where Ctrl+A moved cursor instead of selecting, and Ctrl+C showed "failed to copy cell" error when copying from query input. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): add VerifyPersistence() to ensure MSAL token cache persists The MSAL token cache file was never created because CreateAndRegisterCacheAsync() didn't call VerifyPersistence() after creating the MsalCacheHelper. Without this call, MSAL.Extensions silently fails to persist tokens - the cache registration "succeeds" but no file is written. Added VerifyPersistence() call which performs a write/read/clear test and throws MsalCachePersistenceException if persistence is unavailable. Also improved diagnostic logging to show verification status and file existence. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use selection properties directly for Ctrl+A select all Previous approach using ProcessKey didn't work. Now directly set SelectionStartColumn/Row to (0,0) and CursorPosition to document end. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): handle Ctrl+A directly on TextView to intercept before default Terminal.Gui's TextView handles Ctrl+A internally (move to start). By attaching handler directly to _queryInput.KeyPress, we intercept the key before Terminal.Gui's default handling takes over. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): handle Shift+Space/Delete/Backspace in SQL query editor Terminal.Gui doesn't process these key combinations by default. Convert them to their non-shifted equivalents so they work normally. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add Alt/Ctrl+Backspace/Delete for word deletion - Alt+Backspace and Ctrl+Backspace: delete word before cursor - Alt+Delete and Ctrl+Delete: delete word after cursor Standard text editing shortcuts for faster query editing. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent Alt key from focusing menu after Alt+modifier combos When using Alt+Backspace or Alt+Delete for word deletion, the Alt key release was causing Terminal.Gui to focus the menu bar. Added SuppressNextAltMenuFocus() to HotkeyRegistry to intercept the bare Alt key event after it was used as a modifier. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): use reflection to reset MenuBar's openedByAltKey field The MenuBar uses OnKeyDown/OnKeyUp (not KeyPress) for Alt handling. When Alt is pressed, it sets openedByAltKey=true internally. We now use reflection to reset this field when handling Alt+modifier combos, preventing the menu from stealing focus on Alt release. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): explicitly handle Ctrl+Z/Y for undo/redo in query editor Terminal.Gui has built-in undo/redo but something may be blocking the key events. Explicitly intercept and pass through to ProcessKey. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent accidental menu item selection and crash on quit - Block single letter keys when menu dropdown is open to prevent first-letter navigation (e.g., Q selecting Quit in File menu) - Close menu before calling RequestStop() to prevent Terminal.Gui state corruption that caused NullReferenceException on mouse events - Rename "Quit" to "Exit" as defensive measure (E less likely accidental) - Remove undocumented Ctrl+Q from keyboard shortcuts help - Fix test that incorrectly asserted F2 as global shortcut The crash was in Menu.SetParentSetNeedsDisplay() when mouse events arrived after menu state corruption from quitting while menu was open. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): also block Alt+letter when menu is open Alt+S was still selecting "SQL Query" when File menu was open. Now blocks both plain letters and Alt+letter combinations. Ctrl+letter is still allowed for potential shortcuts. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): ensure Alt+F/T/H opens menus even when TextView has focus Terminal.Gui's TextView intercepts Alt+F for emacs-style word navigation, preventing the MenuBar from receiving it. Now we intercept Alt+F/T/H in RootKeyEvent and set focus to MenuBar first, ensuring menus open correctly. Co-Authored-By: Claude Opus 4.5 <[email protected]> * revert: remove SetFocus approach that broke Alt+arrows The SetFocus() call to redirect Alt+F/T/H to MenuBar was causing inconsistent behavior and breaking Alt+arrow word navigation. Alt+F not opening menu when in SQL text box is a Terminal.Gui limitation - TextView's emacs-style bindings consume Alt+F for word-forward navigation. Users can use F9 or mouse to access menus instead. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add Alt+Left/Right for word navigation in SQL editor Alt+Left and Alt+Right now work the same as Ctrl+Left/Right for word-by-word cursor navigation in the SQL query editor. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add token expiry handling with re-authentication dialog Auth error detection (benefits all UIs): - Add AuthenticationErrorDetector for 401/403 detection - Add DataverseAuthenticationException with RequiresReauthentication flag - Update ThrottleDetector to detect and wrap auth errors - Update ExceptionMapper to handle DataverseAuthenticationException TUI-specific handling: - Add ReAuthenticationDialog for session expiry prompt - Add InvalidateAndReauthenticateAsync to InteractiveSession - Handle auth errors in SqlQueryScreen with retry on re-auth Quick fixes: - Fix LastUsedAt persistence in ProfileServiceFactory - Add Object Id and PUID to ProfileDetailsDialog (align with CLI) - Remove Type/Region from EnvironmentDetailsDialog (shown in header badge) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): always update LastUsedAt on profile use Previously, LastUsedAt only updated when HomeAccountId changed (first auth or re-auth). Now the callback fires on every successful connection, so LastUsedAt tracks actual usage. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor(tui): reorganize menu structure for scalability Menu structure now follows consistent UX pattern: - File: Export (context-aware), Exit - [Context Menu]: Screen-specific actions (Query: Execute, History, Filter) - Tools: SQL Query, Data Migration, Solutions, Plugin Traces - Help: Keyboard Shortcuts, About, Error Log This pattern scales as new tools are added (Metadata Explorer, Data Explorer, Import Jobs, etc.) - all go under Tools menu. Export moved from Query menu to File menu (universal convention). Added ExportAction to ITuiScreen interface for context-aware enabling. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): fix profile update and environment persistence Bug 1: LastUsedAt never updated for unnamed profiles - ProfileStore.UpdateProfileAsync used GetByName() which doesn't support bracket notation like "[1]" - Changed to GetByNameOrIndex() to support DisplayIdentifier format Bug 2: Environment not saved when creating new profile - SetActiveProfileAsync() only updated in-memory state - Added call to profileService.SetEnvironmentAsync() to persist environment selection to profile file Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): handle full DisplayIdentifier format in profile lookups GetByNameOrIndex() now parses "[N] Name" format (not just "[N]"), fixing profile switching in TUI and enabling bracket notation in CLI commands. - Enhanced GetByNameOrIndex() to extract index from "[N] ..." patterns - Fixed ProfileSelectorDialog rename re-selection (compare Name not DisplayIdentifier) - Changed GetByName() to GetByNameOrIndex() in CLI select/delete commands - Changed GetByName() to GetByNameOrIndex() in RPC auth/select handler - Changed GetByName() to GetByNameOrIndex() in ProfileServiceFactory - Added unit tests for DisplayIdentifier parsing Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(tui): add FetchXML preview and Cache Type to profile details - Add FetchXmlPreviewDialog for viewing transpiled SQL queries - Add Ctrl+Shift+F hotkey and Query menu item for FetchXML preview - Add Cache Type field to ProfileDetailsDialog (aligns with CLI ppds auth who) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): fix FetchXML preview compile error and update E2E tests - Fix CS8604 error in ShowFetchXmlDialogAsync by using _environmentUrl instead of _deviceCodeCallback (caller already guards for null) - Update E2E tests to use Tools menu navigation instead of removed F2 shortcut - Update test snapshots to match current UI (removed Quit button, etc.) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): enable File > Export menu after query execution The File > Export menu item stayed disabled even after query returned data because ExportAction was evaluated once at screen load time. Fix: Add MenuStateChanged event to ITuiScreen interface. SqlQueryScreen raises this event after LoadResults(), and TuiShell rebuilds the menu bar in response, enabling the Export item. Includes interface contract tests to prevent regression. Closes #405 Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): enforce black text on blue/cyan backgrounds - Fix FetchXmlPreviewDialog: add ReadOnlyText ColorScheme to prevent grey text - Fix FileDialog: change Focus/HotFocus from White to Black on Cyan - Fix StatusBar_Trial: change all attributes to Black on Cyan - Fix Selected: change HotNormal/HotFocus/Disabled to Black on Cyan - Add ValidateBlueBackgroundRule() helper for automated validation - Add TuiColorPaletteTests to fail build on future violations - Add NEVER rule to CLAUDE.md for blue background text color Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): make read-only text white instead of grey Terminal.Gui treats ReadOnly TextViews as Disabled, so the Disabled attribute colors are used. Changed ReadOnlyText.Disabled to White so FetchXML preview and other read-only content is fully visible. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): prevent Query History from triggering unnecessary authentication QueryHistoryService is a local file-based service that doesn't need a Dataverse connection, but GetQueryHistoryServiceAsync() was going through GetServiceProviderAsync() which triggers authentication. This caused the Query History dialog to hang on "Loading..." when auth tokens weren't cached. - Add GetQueryHistoryService() sync method to InteractiveSession (like GetProfileService) - Simplify ShowHistoryDialog() to use the sync method directly Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): show loading spinner when navigating to SQL Query screen Add loading indicator when user clicks SQL Query from main menu. Uses TuiStatusLine's built-in spinner with "Loading SQL Query..." message. Defers screen creation via AddIdle() so UI refreshes before blocking work. The delay is expected (Terminal.Gui component creation + JIT compilation) but users perceive a frozen UI as broken. This provides immediate feedback. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): center 'No data' label in query results Move empty state label to center of results pane instead of bottom. Separate from status label which stays at bottom for row count/shortcuts. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(auth): prevent tests from deleting global MSAL token cache ProfileService.ClearAllAsync() was calling TokenCacheManager.ClearAllCachesAsync() which always deleted the global token cache at %LOCALAPPDATA%\PPDS\msal_token_cache.bin. This meant running unit tests would nuke the user's real auth tokens. Fix: TokenCacheManager now accepts an optional path parameter. ProfileService derives the token cache path from the ProfileStore's directory, so tests using isolated temp directories delete their own cache, not the global one. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): catch MainLoop.Invoke exceptions and log to error service Previously, exceptions in MainLoop.Invoke callbacks would crash the TUI because they execute outside the scope of surrounding try/catch blocks. This defeated the purpose of the error logging infrastructure. Changes: - Add global errorHandler to Application.Run() that catches unhandled exceptions and reports them to TuiErrorService - Add MainLoopExtensions.SafeInvoke helper for wrapped callback execution - Fix DuplicateNameException crash by handling duplicate column names in QueryResultConverter (appends _1, _2, etc. to duplicates) - Wrap SqlQueryScreen.ExecuteQueryAsync callback in try/catch/finally to ensure spinner stops and state resets even on error - Add tests for duplicate column name handling Now exceptions are caught, logged to ~/.ppds/tui-debug.log, and shown via F12 (Error Details dialog) instead of crashing the application. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat(sql): auto-expand lookup, optionset, and boolean columns with *name variants SQL queries now automatically expand lookup, optionset, and boolean columns to show both raw values and formatted display names, matching the behavior of the old VS Code extension. - SELECT ownerid → shows ownerid (GUID) + owneridname (display name) - SELECT statuscode → shows statuscode (int) + statuscodename (label) - SELECT ismanaged → shows ismanaged (bool) + ismanagedname (Yes/No) Virtual column support allows querying *name columns directly: - SELECT owneridname → shows only owneridname (hides base column) - SELECT ownerid, owneridname → shows both (no duplicates) Changes: - QueryValue: Add IsLookup, IsOptionSet, IsBoolean detection properties - SqlToFetchXmlTranspiler: Detect virtual *name columns, transpile base column - SqlQueryResultExpander: Handle auto-expansion and virtual columns - SqlQueryService: Integrate transpiler and expander - Both columns preserve lookup metadata for TUI navigation (Ctrl+O) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: address CodeQL and Copilot code review findings - Remove unused addedVirtualColumns HashSet in SqlQueryResultExpander - Add CancellationToken propagation to ProfileCreationDialog - Wrap screen disposal in try/catch for exception safety in TuiShell - Remove redundant Color casts in TuiColorPalette - Use TryGetValue instead of ContainsKey+indexer in tests Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tui): wrap Pop() in try-catch to satisfy CodeQL dispose check Addresses CodeQL alert about potential missed Dispose if Pop() throws. Both Pop() and Dispose() are now wrapped in nested try-catch blocks to ensure the disposal loop continues even if either operation fails. Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent a026385 commit 87f7b79

File tree

106 files changed

+9915
-1380
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+9915
-1380
lines changed

.claude/commands/test.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,64 @@ Fast, no external dependencies. Default choice.
3434

3535
### TUI Tests
3636

37+
TUI testing has three layers:
38+
1. **TuiUnit** - State assertions (fast, no terminal needed)
39+
2. **tui-e2e** - Visual snapshots using @microsoft/tui-test (needs Node.js)
40+
3. **TuiIntegration** - Integration with FakeXrmEasy (future)
41+
3742
```bash
3843
# Build first
3944
dotnet build src/PPDS.Cli/PPDS.Cli.csproj --no-restore
4045

41-
# TUI unit tests (fast)
46+
# TUI unit tests - state assertions (fast)
4247
dotnet test tests/PPDS.Cli.Tests/PPDS.Cli.Tests.csproj --filter "Category=TuiUnit" --no-build
4348

49+
# TUI visual snapshots (if Node.js available)
50+
# Use --prefix to run npm from the repo root
51+
if (Get-Command node -ErrorAction SilentlyContinue) {
52+
npm test --prefix tests/tui-e2e
53+
}
54+
4455
# TUI integration tests (if unit pass)
4556
dotnet test tests/PPDS.Cli.Tests/PPDS.Cli.Tests.csproj --filter "Category=TuiIntegration" --no-build
4657
```
4758
59+
**Updating snapshots:** If visual changes are intentional, update snapshots:
60+
```bash
61+
npm test --prefix tests/tui-e2e -- --update-snapshots
62+
```
63+
64+
#### TUI Testing Philosophy
65+
66+
TUI tests verify **presentation**, not business logic. CLI and TUI share Application Services (ADR-0015), so service logic is tested once at the CLI layer.
67+
68+
**What TUI E2E tests verify:**
69+
- Does the screen render correctly? (snapshots)
70+
- Do keyboard shortcuts work? (key sequences)
71+
- Does navigation flow correctly? (screen transitions)
72+
- Do errors display properly? (error dialogs)
73+
74+
**What TUI E2E tests do NOT verify:**
75+
- Query execution correctness (CLI tests `ISqlQueryService`)
76+
- Export format validity (CLI tests `IExportService`)
77+
- Authentication flows (CLI tests auth)
78+
79+
**Trust the service layer:** If CLI tests pass, services work. Don't duplicate that coverage.
80+
81+
#### Interpreting Snapshot Diffs
82+
83+
When E2E tests fail with snapshot diffs:
84+
85+
1. **Read the ASCII diff** - Shows expected vs actual terminal output
86+
2. **Identify what's wrong** - Layout shifted? Text missing? Wrong content?
87+
3. **Fix the TUI code** - The rendering issue is in the view/screen code
88+
4. **Re-run tests** - Verify fix with `npm test --prefix tests/tui-e2e`
89+
5. **If change was intentional** - Update snapshots with `--update-snapshots`
90+
91+
Snapshots are the visual spec. They define "correct" appearance. When you see a diff:
92+
- If the "actual" looks wrong → fix the code
93+
- If the "actual" looks right (intentional change) → update the snapshot
94+
4895
### Integration Tests
4996
5097
Requires `.env.local` with Dataverse credentials.
@@ -177,3 +224,4 @@ ppds plugins --help
177224
178225
- ADR-0028: TUI Testing Strategy
179226
- ADR-0029: Testing Strategy
227+
- `tests/tui-e2e/` - TUI visual snapshot tests using @microsoft/tui-test

.claude/commands/tui-review.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# TUI Review
2+
3+
Evaluate TUI screens for UX quality and identify improvements.
4+
5+
## Usage
6+
7+
`/tui-review` - Review the main TUI experience
8+
`/tui-review sql` - Review SQL Query screen specifically
9+
`/tui-review profile` - Review profile management dialogs
10+
`/tui-review [screen]` - Review a specific screen
11+
12+
## Setup
13+
14+
1. Build the CLI:
15+
```bash
16+
dotnet build src/PPDS.Cli/PPDS.Cli.csproj -f net10.0
17+
```
18+
19+
2. Run automated tests first to ensure baseline works:
20+
```bash
21+
dotnet test --filter "Category=TuiUnit"
22+
```
23+
24+
## Review Process
25+
26+
### Step 1: Launch TUI
27+
```bash
28+
.\src\PPDS.Cli\bin\Debug\net10.0\ppds.exe interactive
29+
```
30+
31+
### Step 2: Explore the Target Screen
32+
33+
**Main Menu:** Navigation, menu items, status bar, keyboard shortcuts
34+
**SQL Query (F2):** Query input, execution, results table, pagination, filter, export
35+
**Profile Selector (Alt+P):** Profile list, create/delete, switching
36+
**Environment Selector (Alt+E):** Environment discovery, selection, details
37+
**Error Log (F12):** Error display, details, copy functionality
38+
39+
### Step 3: Test These Interactions
40+
- Keyboard navigation (Tab, arrows, Enter, Escape)
41+
- Hotkeys (F1, F2, Alt+P, Alt+E, Ctrl+Q)
42+
- Mouse clicks (if applicable)
43+
- Error states (what happens when things fail?)
44+
- Empty states (no data, no profiles, etc.)
45+
- Loading states (spinners, status messages)
46+
47+
### Step 4: Capture State (Optional)
48+
Use the state capture API to inspect internal state:
49+
```csharp
50+
// Components implement ITuiStateCapture<T>
51+
var state = screen.CaptureState();
52+
// Inspect: QueryText, StatusText, ResultCount, etc.
53+
```
54+
55+
## Report Format
56+
57+
Structure your findings as:
58+
59+
### What Works Well
60+
- List polished/intuitive aspects
61+
62+
### UX Issues
63+
| Issue | Severity | Description |
64+
|-------|----------|-------------|
65+
| ... | P1/P2/P3 | ... |
66+
67+
**Severity:**
68+
- P1: Blocks core functionality
69+
- P2: Annoying but workable
70+
- P3: Nice to have improvement
71+
72+
### Missing Features
73+
What would users expect that isn't there?
74+
75+
### Suggested Improvements
76+
Specific, actionable recommendations with rationale.
77+
78+
### Comparison to Similar Tools
79+
How does it compare to lazygit, k9s, or other TUI tools?
80+
81+
## Important
82+
83+
- **DO NOT make code changes** - This is evaluation only
84+
- **DO create issues** for P1/P2 findings using `/create-issue`
85+
- Reference `src/PPDS.Cli/Tui/` for screen locations
86+
- Reference `docs/adr/0028_TUI_TESTING_STRATEGY.md` for testing patterns
87+
88+
## Screen Locations
89+
90+
| Screen | File |
91+
|--------|------|
92+
| Main Window | `src/PPDS.Cli/Tui/MainWindow.cs` |
93+
| SQL Query | `src/PPDS.Cli/Tui/Screens/SqlQueryScreen.cs` |
94+
| Profile Selector | `src/PPDS.Cli/Tui/Dialogs/ProfileSelectorDialog.cs` |
95+
| Environment Selector | `src/PPDS.Cli/Tui/Dialogs/EnvironmentSelectorDialog.cs` |
96+
| Status Bar | `src/PPDS.Cli/Tui/Views/TuiStatusBar.cs` |
97+
| Error Details | `src/PPDS.Cli/Tui/Dialogs/ErrorDetailsDialog.cs` |

.github/workflows/tui-e2e.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: TUI E2E Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'src/PPDS.Cli/Tui/**'
8+
- 'tests/tui-e2e/**'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'src/PPDS.Cli/Tui/**'
13+
- 'tests/tui-e2e/**'
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
tui-e2e:
22+
runs-on: windows-latest
23+
timeout-minutes: 30
24+
25+
steps:
26+
- uses: actions/checkout@v6
27+
with:
28+
fetch-depth: 0
29+
30+
- name: Setup .NET
31+
uses: actions/setup-dotnet@v4
32+
with:
33+
dotnet-version: |
34+
8.0.x
35+
9.0.x
36+
10.0.x
37+
38+
- name: Setup Node.js
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: '20'
42+
cache: 'npm'
43+
cache-dependency-path: tests/tui-e2e/package-lock.json
44+
45+
- name: Build PPDS CLI
46+
run: dotnet build src/PPDS.Cli/PPDS.Cli.csproj -c Debug -f net10.0
47+
48+
- name: Install tui-e2e dependencies
49+
working-directory: tests/tui-e2e
50+
run: npm ci
51+
52+
- name: Install Playwright browsers
53+
working-directory: tests/tui-e2e
54+
run: npx playwright install --with-deps chromium
55+
56+
- name: Run TUI E2E tests
57+
working-directory: tests/tui-e2e
58+
run: npm test
59+
60+
- name: Upload test results
61+
uses: actions/upload-artifact@v4
62+
if: always()
63+
with:
64+
name: tui-e2e-results
65+
path: |
66+
tests/tui-e2e/playwright-report/
67+
tests/tui-e2e/test-results/
68+
retention-days: 7
69+
70+
- name: Upload snapshots on failure
71+
uses: actions/upload-artifact@v4
72+
if: failure()
73+
with:
74+
name: tui-e2e-snapshot-diff
75+
path: tests/tui-e2e/__snapshots__/
76+
retention-days: 7

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ SDK, CLI, TUI, VS Code Extension, and MCP server for Power Platform development.
2929
| Throw raw exceptions from Application Services | Wrap in `PpdsException` with ErrorCode/UserMessage (ADR-0026) |
3030
| Use comma-separated issues in `Closes` | GitHub only auto-closes first; use separate `Closes #N` lines |
3131
| Add TUI service code without tests | Use MockServiceProviderFactory for testability (ADR-0028) |
32+
| Re-test service logic in TUI E2E tests | CLI tests verify services; TUI tests verify presentation/navigation (ADR-0028) |
3233
| Use bash-specific syntax in C# process commands | `2>/dev/null`, `||`, pipes don't work on Windows; handle errors in code |
3334
| File issues in wrong repo | Issues belong in target repo (ppds-docs issues in ppds-docs, not ppds) |
3435
| Start implementation without plan citations | Cite `docs/patterns/` or ADRs in plan's "Patterns I'll Follow" section |
3536
| Omit "What I'm NOT Doing" from plans | Explicit boundaries prevent scope creep; required for approval |
3637
| Implement in design sessions | Design sessions produce plans and issues; workers implement |
38+
| Use non-black text on blue/cyan TUI backgrounds | Black is the only readable color; see `TuiColorPalette.ValidateBlueBackgroundRule()` |
3739

3840
## ALWAYS
3941

docs/adr/0028_TUI_TESTING_STRATEGY.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,46 @@ public InteractiveSession(
5959
- `FakeExportService` - Tracks export operations
6060
- `TempProfileStore` - Isolated ProfileStore with temp directory
6161

62+
### Test Responsibility Matrix
63+
64+
CLI and TUI share Application Services (ADR-0015). This means service logic is tested once at the CLI/service layer, not duplicated in TUI tests.
65+
66+
| Concern | Tested By | Why |
67+
|---------|-----------|-----|
68+
| Query execution logic | CLI/Service tests | Shared `ISqlQueryService` |
69+
| Data transformation | CLI/Service tests | Service layer responsibility |
70+
| Export format validity | CLI/Service tests | Shared `IExportService` |
71+
| Authentication flows | CLI/Service tests | Shared auth infrastructure |
72+
| Screen rendering | TUI E2E snapshots | Visual verification |
73+
| Keyboard navigation | TUI E2E | TUI-specific behavior |
74+
| Session state management | TuiUnit | TUI-specific state (profile switching, caching) |
75+
| Error dialog display | TUI E2E | Presentation of errors |
76+
| Threading/UI loop | TUI E2E | TUI-specific concurrency |
77+
78+
**Principle:** If CLI tests pass, the service works. TUI tests verify the presentation layer only.
79+
80+
### TUI Development Workflow
81+
82+
When implementing TUI features:
83+
84+
1. **Implement** the feature in code
85+
2. **Run TuiUnit** tests for logic: `dotnet test --filter Category=TuiUnit`
86+
3. **Run E2E** tests: `npm test --prefix tests/tui-e2e`
87+
4. **Read snapshot diffs** - they show exactly what's rendering
88+
5. **If diff shows bug** → fix code → re-run
89+
6. **If diff shows intentional change** → update snapshots: `npm test --prefix tests/tui-e2e -- --update-snapshots`
90+
7. **Repeat** until tests pass
91+
92+
### What TUI Tests Should NOT Verify
93+
94+
Do not write TUI E2E tests for:
95+
- Query results are correct (CLI tests `ISqlQueryService`)
96+
- Export format is valid (CLI tests `IExportService`)
97+
- Authentication works (CLI tests auth flows)
98+
- Bulk operations succeed (CLI tests `BulkOperationExecutor`)
99+
100+
These are already covered by CLI/service tests. Duplicating them in TUI E2E wastes effort and creates maintenance burden.
101+
62102
## Consequences
63103

64104
### Positive

src/PPDS.Auth/Credentials/MsalClientBuilder.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ public static IPublicClientApplication CreateClient(
7474
IPublicClientApplication client,
7575
bool warnOnFailure = true)
7676
{
77+
var cacheFilePath = System.IO.Path.Combine(ProfilePaths.DataDirectory, ProfilePaths.TokenCacheFileName);
78+
7779
try
7880
{
7981
ProfilePaths.EnsureDirectoryExists();
8082

81-
var cacheFilePath = System.IO.Path.Combine(ProfilePaths.DataDirectory, ProfilePaths.TokenCacheFileName);
8283
var cacheFileExists = System.IO.File.Exists(cacheFilePath);
8384
var cacheFileSize = cacheFileExists ? new System.IO.FileInfo(cacheFilePath).Length : 0;
8485

@@ -91,14 +92,24 @@ public static IPublicClientApplication CreateClient(
9192
.Build();
9293

9394
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties).ConfigureAwait(false);
95+
96+
// Verify persistence works before registering - performs write/read/clear test
97+
// Throws MsalCachePersistenceException if persistence is unavailable
98+
cacheHelper.VerifyPersistence();
99+
AuthDebugLog.WriteLine("[MsalCache] Persistence verification PASSED");
100+
94101
cacheHelper.RegisterCache(client.UserTokenCache);
95102

96-
AuthDebugLog.WriteLine("[MsalCache] Cache registered successfully");
103+
cacheFileExists = System.IO.File.Exists(cacheFilePath);
104+
AuthDebugLog.WriteLine($"[MsalCache] Cache registered successfully. File exists: {cacheFileExists}");
105+
97106
return cacheHelper;
98107
}
99108
catch (MsalCachePersistenceException ex)
100109
{
101-
AuthDebugLog.WriteLine($"[MsalCache] Cache registration FAILED: {ex.Message}");
110+
AuthDebugLog.WriteLine($"[MsalCache] Persistence verification FAILED: {ex.Message}");
111+
AuthDebugLog.WriteLine($"[MsalCache] Cache file path: {cacheFilePath}");
112+
102113
if (warnOnFailure)
103114
{
104115
AuthenticationOutput.WriteLine(

src/PPDS.Auth/Credentials/TokenCacheManager.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ public static class TokenCacheManager
1212
/// <summary>
1313
/// Clears the MSAL file-based token cache.
1414
/// </summary>
15+
/// <param name="tokenCachePath">
16+
/// Optional path to the token cache file. If null, uses the default global path.
17+
/// Pass a custom path when using isolated test directories to avoid clearing production cache.
18+
/// </param>
1519
/// <remarks>
1620
/// This method deletes the unprotected file-based token cache used by the CLI.
1721
/// The cache file location matches <see cref="MsalClientBuilder.CreateAndRegisterCacheAsync"/>.
1822
/// </remarks>
19-
public static Task ClearAllCachesAsync()
23+
public static Task ClearAllCachesAsync(string? tokenCachePath = null)
2024
{
21-
if (File.Exists(ProfilePaths.TokenCacheFile))
25+
var path = tokenCachePath ?? ProfilePaths.TokenCacheFile;
26+
if (File.Exists(path))
2227
{
23-
File.Delete(ProfilePaths.TokenCacheFile);
28+
File.Delete(path);
2429
}
2530

2631
return Task.CompletedTask;

0 commit comments

Comments
 (0)