Skip to content

Commit c1a6e99

Browse files
feat: terminal rendering improvements (#5)
* docs: add AGENTS.md and opencode settings * fix(xterm): Improve rendering stability and text descender handling - Fix text descender clipping by removing hardcoded line height - Add full-line background painting to reduce horizontal line gaps - Implement pixel-aligned cell size calculation (floor width, ceil height) - Add anti-aliasing disabled flag to prevent edge artifacts - Refactor terminal resize handling to use onResize callback - Move padding from Container to TerminalView for proper layout - Add theme pre-loading to avoid flash of default theme on startup - Improve line positioning calculation for consistent renderin * fix: copilot suggestion applied
1 parent b8ef0c5 commit c1a6e99

12 files changed

Lines changed: 540 additions & 41 deletions

File tree

.opencode/opencode.jsonc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"mcp": {
4+
"dart": {
5+
"type": "local",
6+
"command": ["dart", "mcp-server"]
7+
}
8+
}
9+
}

AGENTS.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# FLUTTER AGENT PANEL - PROJECT KNOWLEDGE BASE
2+
3+
**Generated:** 2026-01-08
4+
**Commit:** b8ef0c5
5+
**Branch:** main
6+
7+
## OVERVIEW
8+
Cross-platform desktop terminal aggregator with AI agent integration. Built with Flutter + shadcn_ui, using BLoC/HydratedBloc for state persistence.
9+
10+
## STRUCTURE
11+
```
12+
flutter_agent_panel/
13+
├── lib/
14+
│ ├── main.dart # Entry: error handling, HydratedStorage init
15+
│ ├── app.dart # Root widget: MultiBlocProvider, theming
16+
│ ├── core/ # Cross-cutting: services, router, l10n, extensions
17+
│ ├── features/ # Feature modules (BLoC pattern)
18+
│ │ ├── home/ # App shell layout
19+
│ │ ├── info/ # About/Help dialogs
20+
│ │ ├── settings/ # App configuration (879-line dialog)
21+
│ │ ├── terminal/ # PTY management, themes
22+
│ │ └── workspace/ # Multi-workspace organization
23+
│ └── shared/ # Utils, constants, common widgets
24+
├── packages/
25+
│ ├── flutter_pty/ # FFI PTY bindings (ConPTY/POSIX)
26+
│ └── xterm/ # Terminal emulator core (60fps render)
27+
└── assets/ # Images, agent logos
28+
```
29+
30+
## WHERE TO LOOK
31+
32+
| Task | Location | Notes |
33+
|------|----------|-------|
34+
| Add new feature | `lib/features/{name}/` | Create bloc/, models/, views/, widgets/ |
35+
| Modify theming | `lib/app.dart` | ShadThemeData, colorScheme switching |
36+
| Add localization | `lib/core/l10n/` | ARB files, regenerate with `flutter gen-l10n` |
37+
| Configure routing | `lib/core/router/app_router.dart` | AutoRoute, regenerate with `lean_builder` |
38+
| Add service | `lib/core/services/` | Singleton pattern, init in main.dart |
39+
| Terminal rendering | `packages/xterm/lib/src/ui/` | Custom RenderBox, pixel-aligned painting |
40+
| PTY native code | `packages/flutter_pty/src/` | C files per platform |
41+
42+
## CONVENTIONS
43+
44+
### State Management
45+
- **HydratedBloc** for persistent state (workspace, settings)
46+
- **Regular Bloc** for ephemeral state (terminal instances)
47+
- Event/State in `part` files: `{name}_event.dart`, `{name}_state.dart`
48+
- Always use `.copyWith()` for immutable updates
49+
50+
### UI Patterns
51+
- **shadcn_ui** components: ShadDialog, ShadSelect, ShadInput, ShadTooltip
52+
- **Context extensions**: `context.theme`, `context.t` (localization)
53+
- **LucideIcons** throughout
54+
- **Gap** widgets for spacing (not SizedBox)
55+
56+
### Code Style
57+
- Single quotes for strings
58+
- Trailing commas required
59+
- Relative imports within lib/
60+
- `@pragma('vm:prefer-inline')` on hot paths (xterm painter)
61+
62+
## ANTI-PATTERNS (THIS PROJECT)
63+
64+
| Forbidden | Reason |
65+
|-----------|--------|
66+
| Direct list mutation | Use `.copyWith()` or create new list |
67+
| Heavy logic in `build()` | Move to Bloc or extract methods |
68+
| Inline sorting | Delegate to Bloc events |
69+
| Hardcoded strings | Use `context.t` localized strings |
70+
| Manual mock edits | `*.mocks.dart` are generated |
71+
| `flutter_pty_bindings_generated.dart` edits | Auto-generated by ffigen |
72+
| Sub-pixel offsets in painter | Use `roundToDouble()` for crispness |
73+
| Blocking PTY calls on main isolate | Use dedicated isolates |
74+
75+
## COMMANDS
76+
```bash
77+
# Development
78+
flutter pub get
79+
dart run lean_builder build # Generate routes
80+
flutter gen-l10n # Generate localizations
81+
flutter run -d windows # Run on Windows
82+
83+
# Packages (run from package dir)
84+
flutter pub run ffigen --config ffigen.yaml # Regenerate PTY bindings
85+
86+
# Build
87+
flutter build windows
88+
flutter build macos
89+
flutter build linux
90+
```
91+
92+
## NOTES
93+
94+
### Desktop-Only
95+
No Android/iOS directories - configured for Windows, macOS, Linux only.
96+
97+
### Monorepo Structure
98+
Uses Flutter workspace with local packages:
99+
- `packages/xterm` - forked/customized terminal emulator
100+
- `packages/flutter_pty` - native PTY bindings
101+
102+
### Large File Hotspots
103+
- `settings_dialog.dart` (879 lines) - Tab-based settings UI
104+
- `workspace_drawer.dart` (541 lines) - Drag-drop with pin constraints
105+
- `terminal_bloc.dart` (434 lines) - Cross-platform shell handling
106+
107+
### Platform-Specific Shell Logic
108+
Terminal creation handles: PowerShell, pwsh, cmd, WSL, bash, zsh. See `TerminalServiceImpl` and `TerminalBloc._createTerminalNode()`.
109+
110+
### Inter-Feature Dependencies
111+
```
112+
workspace → terminal (TerminalConfig model)
113+
settings → terminal (font/theme models)
114+
terminal → flutter_pty, xterm packages
115+
```

lib/core/AGENTS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# lib/core - AGENT GUIDE
2+
3+
## OVERVIEW
4+
Cross-cutting concerns: services, routing, localization, extensions.
5+
6+
## STRUCTURE
7+
```
8+
core/
9+
├── services/
10+
│ ├── terminal_service.dart # PTY startup abstraction
11+
│ ├── app_logger.dart # Singleton Logger wrapper
12+
│ ├── crash_log_service.dart # Error persistence to disk
13+
│ ├── user_config_service.dart # Config directory paths
14+
│ ├── app_version_service.dart # Version check + updates
15+
│ └── app_bloc_observer.dart # BLoC event/transition logging
16+
├── router/
17+
│ ├── app_router.dart # AutoRoute configuration
18+
│ └── app_router.gr.dart # GENERATED - do not edit
19+
├── l10n/
20+
│ ├── app_localizations.dart # GENERATED base class
21+
│ ├── app_localizations_en.dart # GENERATED
22+
│ └── app_localizations_zh.dart # GENERATED
23+
├── extensions/
24+
│ └── context_extension.dart # context.theme, context.t
25+
├── constants/
26+
│ └── assets.dart # Asset path constants
27+
└── types/
28+
└── typedefs.dart # Callback type definitions
29+
```
30+
31+
## WHERE TO LOOK
32+
33+
| Task | File | Notes |
34+
|------|------|-------|
35+
| Add new service | `services/` | Singleton pattern, init in main.dart |
36+
| Add route | `router/app_router.dart` | Run `dart run lean_builder build` |
37+
| Add translation | `assets/l10n/*.arb` | Run `flutter gen-l10n` |
38+
| Add extension | `extensions/context_extension.dart` | On BuildContext |
39+
40+
## CONVENTIONS
41+
42+
### Services
43+
```dart
44+
// Singleton pattern used throughout
45+
class MyService {
46+
MyService._();
47+
static final MyService instance = MyService._();
48+
49+
Future<void> init() async { ... }
50+
}
51+
```
52+
53+
### Context Extensions
54+
```dart
55+
// Access anywhere in widget tree
56+
context.theme // ShadThemeData
57+
context.t // AppLocalizations
58+
context.colorScheme
59+
context.textTheme
60+
```
61+
62+
### Generated Files (DO NOT EDIT)
63+
- `app_router.gr.dart` - regenerate with `lean_builder`
64+
- `app_localizations*.dart` - regenerate with `flutter gen-l10n`
65+
66+
## ANTI-PATTERNS
67+
68+
| Forbidden | Reason |
69+
|-----------|--------|
70+
| Edit `*.gr.dart` | Auto-generated by lean_builder |
71+
| Edit `app_localizations*.dart` | Auto-generated by flutter gen-l10n |
72+
| Service without singleton | Inconsistent state across app |
73+
| Direct Logger() usage | Use AppLogger.instance.logger |

lib/features/settings/AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# lib/features/settings - AGENT GUIDE
2+
3+
## OVERVIEW
4+
App configuration management: themes, fonts, shells, agents, localization.
5+
6+
## STRUCTURE
7+
```
8+
settings/
9+
├── bloc/
10+
│ ├── settings_bloc.dart # HydratedBloc (persistent)
11+
│ ├── settings_event.dart # 15+ event types
12+
│ └── settings_state.dart # AppSettings wrapper
13+
├── models/
14+
│ ├── app_settings.dart # Main config model (JSON serializable)
15+
│ ├── agent_config.dart # AI agent definitions
16+
│ ├── custom_shell_config.dart
17+
│ ├── terminal_font_settings.dart
18+
│ ├── app_theme.dart # Light/Dark enum
19+
│ └── shell_type.dart # PowerShell/Bash/WSL/Custom
20+
├── views/
21+
│ └── settings_dialog.dart # 879-line tab dialog (COMPLEXITY HOTSPOT)
22+
└── widgets/
23+
├── agents_content.dart # Agent management + installation
24+
├── appearance_settings_content.dart
25+
├── general_settings_content.dart
26+
├── custom_shells_content.dart
27+
├── update_settings_content.dart
28+
├── agent_dialog.dart # Add/edit agent
29+
├── shell_dialog.dart # Add/edit custom shell
30+
└── settings_section.dart # Reusable layout wrapper
31+
```
32+
33+
## WHERE TO LOOK
34+
35+
| Task | File | Notes |
36+
|------|------|-------|
37+
| Add new setting | `models/app_settings.dart` | Add field + copyWith + JSON |
38+
| Add settings tab | `views/settings_dialog.dart` | `_buildContentForIndex()` switch |
39+
| New agent preset | `models/app_settings.dart` | `getDefaultAgents()` static |
40+
| Persist new field | `bloc/settings_bloc.dart` | Add event handler + fromJson/toJson |
41+
42+
## CONVENTIONS
43+
44+
- **Persistence**: HydratedBloc auto-saves to `storage/` directory
45+
- **Clear nullable fields**: Use `clearX: true` pattern in copyWith
46+
- **Tab content**: Each tab is a separate `*_content.dart` widget
47+
- **Dialogs**: ShadDialog with ShadInput/ShadSelect components
48+
- **Validation**: Inline in dialog widgets before emitting events
49+
50+
## ANTI-PATTERNS
51+
52+
| Forbidden | Do Instead |
53+
|-----------|------------|
54+
| Add logic to settings_dialog.dart | Create new widget in widgets/ |
55+
| Direct AppSettings mutation | Emit SettingsEvent through Bloc |
56+
| Hardcode agent commands | Use AgentConfig model with env vars |
57+
58+
## COMPLEXITY NOTES
59+
60+
`settings_dialog.dart` is 879 lines due to 6 tab sections. Each section handles:
61+
- Async font/theme loading
62+
- File picker for custom themes
63+
- Agent installation with toast feedback
64+
- JSON validation for custom terminal themes
65+
66+
Consider splitting if adding more tabs.

lib/features/terminal/AGENTS.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# lib/features/terminal - AGENT GUIDE
2+
3+
## OVERVIEW
4+
PTY lifecycle management with cross-platform shell support and terminal theming.
5+
6+
## STRUCTURE
7+
```
8+
terminal/
9+
├── bloc/
10+
│ ├── terminal_bloc.dart # PTY creation, shell resolution (434 lines)
11+
│ ├── terminal_event.dart # Create/Kill/Restart/Resize events
12+
│ └── terminal_state.dart # TerminalNode map management
13+
├── models/
14+
│ ├── terminal_node.dart # PTY + Terminal + Controller bundle
15+
│ ├── terminal_config.dart # Shell type, working dir, env vars
16+
│ ├── terminal_theme_data.dart
17+
│ └── built_in_themes.dart # 20+ predefined color schemes
18+
├── services/
19+
│ ├── isolate_pty.dart # Background PTY I/O handling
20+
│ └── terminal_theme_service.dart # Theme JSON parsing
21+
├── views/
22+
│ ├── terminal_view.dart # BlocBuilder + TerminalComponent
23+
│ └── terminal_component.dart # xterm TerminalView wrapper
24+
└── widgets/
25+
├── terminal_search_bar.dart # Regex search with navigation
26+
├── activity_indicator.dart # Output activity pulse
27+
└── glowing_icon.dart # Agent status indicator
28+
```
29+
30+
## WHERE TO LOOK
31+
32+
| Task | File | Notes |
33+
|------|------|-------|
34+
| Add shell type | `terminal_bloc.dart` | `_createTerminalNode()` switch |
35+
| Custom theme | `terminal_theme_service.dart` | JSON parsing logic |
36+
| PTY resize | `terminal_bloc.dart` | `_onResizeTerminal()` |
37+
| Search feature | `widgets/terminal_search_bar.dart` | TerminalSearchController |
38+
39+
## CONVENTIONS
40+
41+
- **TerminalNode**: Bundles Pty + Terminal + TerminalController
42+
- **Shell resolution**: Try pwsh → powershell → bash fallback
43+
- **Environment**: Always set `TERM=xterm-256color`, `LANG=en_US.UTF-8`
44+
- **Isolates**: PTY output streams on dedicated isolates
45+
46+
## PLATFORM-SPECIFIC LOGIC
47+
48+
```dart
49+
// Windows shells
50+
'powershell.exe' → ['-NoLogo', '-ExecutionPolicy', 'Bypass']
51+
'pwsh.exe' → ['-NoLogo', '-ExecutionPolicy', 'Bypass']
52+
'cmd.exe' → []
53+
'wsl.exe' → ['--', 'bash', '-l']
54+
55+
// Unix shells
56+
'/bin/bash' → ['-l']
57+
'/bin/zsh' → ['-l']
58+
```
59+
60+
## ANTI-PATTERNS
61+
62+
| Forbidden | Reason |
63+
|-----------|--------|
64+
| PTY calls on main isolate | UI jank - use IsolatePty |
65+
| Direct Terminal state access | Use TerminalController |
66+
| Hardcoded theme colors | Use TerminalThemeData model |
67+
68+
## INTER-FEATURE DEPENDENCIES
69+
70+
- **Imports from settings**: `TerminalFontSettings`, `AppSettings.terminalThemeName`
71+
- **Used by workspace**: `TerminalConfig` stored in Workspace model
72+
- **Uses packages**: `flutter_pty` (PTY), `xterm` (rendering)

lib/features/terminal/bloc/terminal_bloc.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,12 @@ class TerminalBloc extends Bloc<TerminalEvent, TerminalState> {
420420
pty.write(const Utf8Encoder().convert(data));
421421
};
422422

423+
// Setup Terminal -> PTY (Resize)
424+
// This is called by xterm's RenderTerminal when autoResize is enabled
425+
terminal.onResize = (width, height, pixelWidth, pixelHeight) {
426+
node.resize(width, height);
427+
};
428+
423429
return node;
424430
}
425431

0 commit comments

Comments
 (0)