Skip to content

Commit e317e58

Browse files
joshsmithxrmclaude
andauthored
feat: migrate interactive mode to Terminal.Gui TUI framework (#242)
* docs: add design doc for TUI enhancements Issues: #204, #205, #206, #207, #208, #234 Branch: feature/tui-enhancements * feat: migrate interactive mode to Terminal.Gui TUI framework - Add Terminal.Gui 1.19 NuGet package for TUI rendering - Create TUI application structure (PpdsApplication, MainWindow, SqlQueryScreen) - Create QueryResultsTableView with pagination, copy, and URL features - Extract QueryResultConverter for testable value formatting logic - Update InteractiveCommand to launch Terminal.Gui app Architecture documentation (ADRs): - ADR-0024: Shared Local State Architecture (~/.ppds/ for all UIs) - ADR-0025: UI-Agnostic Progress Reporting (IProgressReporter pattern) - ADR-0026: Structured Error Model (PpdsException hierarchy) Updates CLAUDE.md with Platform Architecture section explaining the multi-interface platform (CLI, TUI, VS Code extension). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address bot review comments for TUI implementation - Pass cancellationToken from InteractiveCommand to PpdsApplication - Register cancellation token to stop Terminal.Gui application - Add proper error handling for fire-and-forget async in constructor - Use Application.MainLoop.Invoke() for UI updates from async methods - Add guard flag to prevent concurrent LoadMoreAsync calls - Replace generic catch clauses with specific exceptions (InvalidOperationException, HttpRequestException) - Add documentation comment explaining sync-over-async in Dispose 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ffbb2e0 commit e317e58

File tree

13 files changed

+2068
-18
lines changed

13 files changed

+2068
-18
lines changed

.claude/design.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Design: TUI Enhancements
2+
3+
## Issues
4+
5+
| # | Title | Type | Priority | Size |
6+
|---|-------|------|----------|------|
7+
| 204 | TUI: Add search/filter within loaded results | feature | P2-Medium | M |
8+
| 205 | TUI: Add multi-cell selection with Shift+Arrow | feature | P2-Medium | M |
9+
| 206 | TUI: Export selection as CSV/TSV | feature | P2-Medium | S |
10+
| 207 | TUI: Add mouse support for table navigation | feature | P3-Low | S |
11+
| 208 | TUI: Query history as selectable list | feature | P2-Medium | M |
12+
| 234 | refactor(tui): Abstract SQL table pattern for reuse across all data tables | refactor | P2-Medium | L |
13+
14+
## Context
15+
16+
The TUI (Terminal User Interface) provides an interactive SQL query experience for Dataverse. Current implementation works but lacks ergonomic features that would improve daily workflow.
17+
18+
### Current TUI Location
19+
```
20+
src/PPDS.Cli/Tui/
21+
├── SqlQueryScreen.cs # Main query interface
22+
├── DataTableView.cs # Table display component
23+
├── QueryInputView.cs # SQL input component
24+
└── ...
25+
```
26+
27+
## Feature Dependencies
28+
29+
```
30+
#234 (Abstract table pattern) ─┬─► #204 (Search/filter)
31+
├─► #205 (Multi-cell selection)
32+
├─► #206 (Export CSV)
33+
└─► #207 (Mouse support)
34+
35+
#208 (Query history) ──► Independent
36+
```
37+
38+
**Recommendation:** Do #234 first to establish the abstraction, then others can build on it.
39+
40+
## Suggested Implementation Order
41+
42+
1. **#234** - Abstract SQL table pattern (foundation for others)
43+
2. **#208** - Query history (independent, can be parallel)
44+
3. **#204** - Search/filter within results
45+
4. **#205** - Multi-cell selection
46+
5. **#206** - Export CSV (needs #205 for selection)
47+
6. **#207** - Mouse support (nice-to-have)
48+
49+
## Key Files
50+
51+
```
52+
src/PPDS.Cli/Tui/SqlQueryScreen.cs
53+
src/PPDS.Cli/Tui/DataTableView.cs
54+
src/PPDS.Cli/Tui/QueryInputView.cs
55+
src/PPDS.Cli/Services/QueryHistoryService.cs (new)
56+
```
57+
58+
## Technical Notes
59+
60+
### #234 - Table Abstraction
61+
- Extract generic `DataTableView<T>` from current SQL-specific implementation
62+
- Should support: column definitions, row data, selection, scrolling
63+
- Reusable for: query results, metadata listings, import previews
64+
65+
### #204 - Search/Filter
66+
- `/` key to enter search mode (vim-style)
67+
- Filter rows by text match across all visible columns
68+
- `n`/`N` to navigate between matches
69+
- `Esc` to clear filter
70+
71+
### #205 - Multi-Cell Selection
72+
- `Shift+Arrow` to extend selection
73+
- `Ctrl+A` to select all
74+
- Visual highlight for selected cells
75+
- Store selection state for export
76+
77+
### #206 - Export CSV
78+
- `Ctrl+E` or command to export
79+
- Export current selection or all if no selection
80+
- Prompt for file path or use clipboard
81+
- Support both CSV and TSV formats
82+
83+
### #207 - Mouse Support
84+
- Terminal.Gui supports mouse events
85+
- Click to select cell
86+
- Drag to select range
87+
- Scroll wheel for navigation
88+
89+
### #208 - Query History
90+
- Store last N queries (configurable, default 100)
91+
- `Ctrl+R` for reverse search (like bash)
92+
- Arrow up/down in query input to cycle history
93+
- Persist to `~/.ppds/query-history.json`
94+
95+
## Acceptance Criteria
96+
97+
### #234
98+
- [ ] Generic `DataTableView<T>` component extracted
99+
- [ ] Current SqlQueryScreen uses the abstraction
100+
- [ ] No regression in existing TUI functionality
101+
102+
### #204
103+
- [ ] `/` enters filter mode
104+
- [ ] Filter text shown in status bar
105+
- [ ] Rows filtered in real-time
106+
- [ ] `Esc` clears filter
107+
108+
### #205
109+
- [ ] Shift+Arrow extends selection
110+
- [ ] Selection visually highlighted
111+
- [ ] Selection state accessible programmatically
112+
113+
### #206
114+
- [ ] Export hotkey works
115+
- [ ] CSV format correct (quoted strings, escaped commas)
116+
- [ ] TSV option available
117+
- [ ] Clipboard support if no file specified
118+
119+
### #207
120+
- [ ] Click selects cell
121+
- [ ] Drag selects range
122+
- [ ] Scroll wheel scrolls table
123+
124+
### #208
125+
- [ ] History persisted across sessions
126+
- [ ] Arrow keys cycle through history
127+
- [ ] Ctrl+R reverse search works
128+
- [ ] Duplicate queries collapsed

CLAUDE.md

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ NuGet packages & CLI for Power Platform: plugin attributes, Dataverse connectivi
66

77
| Rule | Why |
88
|------|-----|
9-
| Commit directly to main | Protected branch; always create branch + PR |
10-
| Regenerate `PPDS.Plugins.snk` | Breaks strong naming |
11-
| Create new ServiceClient per request | 42,000x slower than pool |
12-
| Hold single pooled client for multiple queries | Defeats pool parallelism |
13-
| Use magic strings for generated entities | Use `EntityLogicalName` and `Fields.*` |
14-
| Write CLI status messages to stdout | stdout = data, stderr = status |
9+
| Commit directly to `main` | Branch is protected; all changes require PR |
10+
| Regenerate `PPDS.Plugins.snk` | Breaks strong naming; existing assemblies won't load |
11+
| Skip XML documentation on public APIs | Consumers need IntelliSense documentation |
12+
| Commit with failing tests | All tests must pass before merge |
13+
| Create new ServiceClient per request | 42,000x slower than Clone/pool pattern |
14+
| Guess parallelism values | Use `RecommendedDegreesOfParallelism` from server |
15+
| Hold single pooled client for multiple queries | Defeats pool parallelism; see `.claude/rules/DATAVERSE_PATTERNS.md` |
16+
| Use magic strings for generated entities | Use `EntityLogicalName` and `Fields.*` constants |
17+
| Use late-bound `Entity` for generated entity types | Use early-bound classes; compile-time safety |
18+
| Write CLI status messages to stdout | Use `Console.Error.WriteLine` for status; stdout is for data |
19+
| Access `~/.ppds/` files directly from UI code | Use Application Services; they handle caching, locking (ADR-0024) |
20+
| Implement data/business logic in UI layer | UIs are dumb views; logic belongs in Application Services |
21+
| Write progress directly to console from services | Accept `IProgressReporter`; let UI render (ADR-0025) |
22+
| Throw raw exceptions from Application Services | Wrap in `PpdsException` with ErrorCode/UserMessage (ADR-0026) |
1523

1624
## ALWAYS
1725

@@ -20,20 +28,97 @@ NuGet packages & CLI for Power Platform: plugin attributes, Dataverse connectivi
2028
| Use connection pool for multi-request scenarios | See `.claude/rules/DATAVERSE_PATTERNS.md` |
2129
| Use bulk APIs (`CreateMultiple`, `UpdateMultiple`) | 5x faster than `ExecuteMultiple` |
2230
| Add new services to `RegisterDataverseServices()` | Keeps CLI and library DI in sync |
31+
| Use Application Services for all persistent state | Single code path for CLI/TUI/RPC (ADR-0024) |
32+
| Accept `IProgressReporter` for operations >1 second | All UIs need feedback for long operations (ADR-0025) |
33+
| Include ErrorCode in `PpdsException` | Enables programmatic handling (retry, re-auth) (ADR-0026) |
34+
| Make new user data accessible via `ppds serve` | VS Code extension needs same data as CLI/TUI |
2335

24-
## Structure
36+
---
37+
38+
## 💻 Tech Stack
39+
40+
| Technology | Version | Purpose |
41+
|------------|---------|---------|
42+
| .NET | 4.6.2, 8.0, 9.0, 10.0 | Plugins: 4.6.2 only; libraries/CLI: 8.0+ |
43+
| C# | Latest (LangVersion) | Primary language |
44+
| Strong Naming | .snk file | Required for Dataverse plugin assemblies |
45+
| Terminal.Gui | 1.19+ | TUI application framework |
46+
| Spectre.Console | 0.54+ | CLI command output |
47+
48+
---
49+
50+
## 📁 Project Structure
51+
52+
```
53+
ppds-sdk/
54+
├── src/
55+
│ ├── PPDS.Plugins/ # Plugin attributes (PluginStep, PluginImage)
56+
│ ├── PPDS.Dataverse/ # Connection pool, bulk operations, metadata
57+
│ │ └── Generated/ # Early-bound entity classes (DO NOT edit)
58+
│ ├── PPDS.Migration/ # Migration engine library
59+
│ ├── PPDS.Auth/ # Authentication profiles
60+
│ └── PPDS.Cli/ # CLI tool (ppds command)
61+
│ ├── Commands/ # CLI command handlers
62+
│ ├── Services/ # Application Services (ADR-0015)
63+
│ └── Tui/ # Terminal.Gui application
64+
├── tests/ # Unit, integration, and live tests
65+
├── docs/adr/ # Architecture Decision Records
66+
└── CHANGELOG.md
67+
```
68+
69+
## 🏛️ Platform Architecture
70+
71+
PPDS is a **multi-interface platform**, not just a CLI tool. The TUI is the primary development interface, with VS Code extension and other frontends consuming the same services.
2572

2673
```
27-
src/
28-
├── PPDS.Plugins/ # Plugin attributes (PluginStep, PluginImage)
29-
├── PPDS.Dataverse/ # Connection pool, bulk operations
30-
│ └── Generated/ # Early-bound entities (DO NOT edit)
31-
├── PPDS.Migration/ # Migration engine
32-
├── PPDS.Auth/ # Auth profiles
33-
└── PPDS.Cli/ # CLI (ppds command)
74+
┌─────────────────────────────────────────────────────────────┐
75+
│ User Interfaces │
76+
├───────────────┬───────────────┬───────────────┬─────────────┤
77+
│ CLI Commands │ TUI App │ VS Code Ext │ Future │
78+
│ (ppds data) │ (ppds -i) │ (RPC client) │ (Web, etc) │
79+
│ │ │ │ │
80+
│ Spectre.Console│ Terminal.Gui │ JSON-RPC │ │
81+
├───────────────┴───────────────┴───────────────┴─────────────┤
82+
│ ppds serve (RPC Server) │
83+
│ Long-running service for extensions │
84+
├─────────────────────────────────────────────────────────────┤
85+
│ Application Services Layer (ADR-0015) │
86+
│ ISqlQueryService, IDataMigrationService, IPluginService │
87+
│ • Accepts IProgressReporter (ADR-0025) │
88+
│ • Throws PpdsException (ADR-0026) │
89+
│ • Reads/writes ~/.ppds/ (ADR-0024) │
90+
├─────────────────────────────────────────────────────────────┤
91+
│ PPDS.Dataverse / PPDS.Migration / PPDS.Auth │
92+
└─────────────────────────────────────────────────────────────┘
3493
```
3594

36-
## Generated Entities
95+
### Design Principles
96+
97+
| Principle | Implication |
98+
|-----------|-------------|
99+
| **TUI-first** | Build features in TUI first, then expose via RPC for extensions |
100+
| **Service layer** | All business logic in Application Services, never in UI code |
101+
| **Shared local state** | All UIs access same `~/.ppds/` data via services (ADR-0024) |
102+
| **Framework choice** | CLI: Spectre.Console, TUI: Terminal.Gui, Extension: RPC client |
103+
104+
### Shared Local State
105+
106+
All user data lives in `~/.ppds/` and is accessed via Application Services:
107+
108+
```
109+
~/.ppds/
110+
├── profiles.json # Auth profiles (IProfileService)
111+
├── history/ # Query history per-environment (IQueryHistoryService)
112+
├── settings.json # User preferences (ISettingsService)
113+
├── msal_token_cache.bin # MSAL token cache
114+
└── ppds.credentials.dat # Encrypted credentials
115+
```
116+
117+
**Access pattern:** `CLI/TUI/VSCode → Application Service → ~/.ppds/`
118+
119+
---
120+
121+
## 🏗️ Generated Entities
37122

38123
Early-bound in `src/PPDS.Dataverse/Generated/`. Use `EntityLogicalName` and `Fields.*` constants.
39124

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# ADR-0024: Shared Local State Architecture
2+
3+
**Status:** Accepted
4+
**Date:** 2026-01-06
5+
**Authors:** Josh, Claude
6+
7+
## Context
8+
9+
PPDS is a multi-interface platform with multiple UIs accessing the same user data:
10+
11+
1. **CLI Commands** (`ppds auth list`) - Traditional command-line
12+
2. **TUI Application** (`ppds -i`) - Terminal.Gui interactive mode
13+
3. **VS Code Extension** - Connects via `ppds serve` JSON-RPC daemon
14+
4. **Future UIs** - Web, desktop app, etc.
15+
16+
Without explicit guidance, each UI might implement its own storage for auth profiles, query history, settings, etc. This leads to:
17+
18+
- **Data silos**: Login from TUI not available in CLI
19+
- **Code duplication**: Each UI implementing file I/O
20+
- **Inconsistent behavior**: Different caching, locking, validation
21+
- **Maintenance burden**: Same bugs fixed in multiple places
22+
23+
## Decision
24+
25+
### Single Location
26+
27+
All user data lives in `~/.ppds/` (or `%LOCALAPPDATA%\PPDS` on Windows):
28+
29+
```
30+
~/.ppds/
31+
├── profiles.json # Auth profiles
32+
├── history/ # Query history per-environment
33+
│ ├── {env-hash-1}.json
34+
│ └── {env-hash-2}.json
35+
├── settings.json # User preferences
36+
├── msal_token_cache.bin # MSAL token cache
37+
└── ppds.credentials.dat # Encrypted credentials (DPAPI)
38+
```
39+
40+
### Single Code Path
41+
42+
All access goes through Application Services (ADR-0015). UIs never read/write files directly.
43+
44+
```
45+
CLI: ppds auth list → IProfileService.GetProfilesAsync()
46+
TUI: Profile Selector → IProfileService.GetProfilesAsync()
47+
VS Code: RPC call → ppds serve → IProfileService.GetProfilesAsync()
48+
```
49+
50+
### Stateless UIs
51+
52+
UIs are "dumb views" that:
53+
- Call Application Services for all persistent state
54+
- Never manage file I/O, caching, or locking
55+
- Format service responses for their display medium
56+
57+
### RPC Exposure
58+
59+
`ppds serve` exposes Application Services to remote clients:
60+
- VS Code extension calls RPC methods
61+
- RPC handlers delegate to the same services CLI/TUI use
62+
- No special "remote" code path - same services, different transport
63+
64+
## Consequences
65+
66+
### Positive
67+
68+
- **Login from ANY interface = available in ALL interfaces**
69+
- **No code duplication** - file I/O written once in services
70+
- **Consistent behavior** - same caching, locking, validation
71+
- **Testable** - services can be unit tested without UI
72+
73+
### Negative
74+
75+
- **Requires discipline** - UIs must resist temptation to read files directly
76+
- **Service overhead** - simple reads go through abstraction layer
77+
78+
### Neutral
79+
80+
- **Existing pattern** - `ProfileStore` already follows this model
81+
- **No new package** - services stay in PPDS.Cli
82+
83+
## Implementation Guidelines
84+
85+
### Services Own File Access
86+
87+
```csharp
88+
// CORRECT: Service handles file I/O
89+
public class QueryHistoryService : IQueryHistoryService
90+
{
91+
private readonly string _historyDir = ProfilePaths.GetHistoryDirectory();
92+
private readonly SemaphoreSlim _lock = new(1, 1);
93+
94+
public async Task<IReadOnlyList<HistoryEntry>> GetHistoryAsync(string environmentUrl)
95+
{
96+
await _lock.WaitAsync();
97+
try
98+
{
99+
var path = GetHistoryPath(environmentUrl);
100+
if (!File.Exists(path)) return Array.Empty<HistoryEntry>();
101+
var json = await File.ReadAllTextAsync(path);
102+
return JsonSerializer.Deserialize<HistoryFile>(json)?.Queries ?? [];
103+
}
104+
finally
105+
{
106+
_lock.Release();
107+
}
108+
}
109+
}
110+
```
111+
112+
### UIs Call Services
113+
114+
```csharp
115+
// CORRECT: TUI calls service
116+
var history = await _queryHistoryService.GetHistoryAsync(environmentUrl);
117+
var listView = new ListView { Source = history.Select(h => h.Sql).ToList() };
118+
119+
// WRONG: TUI reads file directly
120+
var json = File.ReadAllText("~/.ppds/history/abc123.json"); // NO!
121+
```
122+
123+
### New Data = New Service
124+
125+
When adding new persistent data:
126+
1. Add file to `~/.ppds/` directory structure
127+
2. Create `I{Name}Service` interface
128+
3. Create `{Name}Service` implementation with file I/O
129+
4. Register in `AddCliApplicationServices()`
130+
5. Expose via RPC in `ppds serve`
131+
132+
## References
133+
134+
- ADR-0015: Application Service Layer for CLI/TUI/Daemon
135+
- ADR-0007: Unified CLI and Auth Profiles
136+
- Existing pattern: `ProfileStore.cs`, `SecureCredentialStore.cs`

0 commit comments

Comments
 (0)