Skip to content

Commit bd365e9

Browse files
committed
Enhance vide_in_app_sdk: file browser, git view, session management, screenshot animations
- Add file browser widget with directory navigation, git status indicators, and diff viewing - Add git view widget with branch info, staged/unstaged changes, and commit history - Add Chat/Files/Git tab switcher to session layout - Add folder picker sheet for browsing server filesystem in settings - Add close button to session header for disconnecting active sessions - Improve empty state: show compact input bar when sessions exist, swipe-to-delete sessions - Smooth screenshot overlay transitions with fade in/out animations - Refresh session list after disconnect so recent sessions appear immediately
1 parent a87adc3 commit bd365e9

12 files changed

Lines changed: 3951 additions & 782 deletions

File tree

lib/modules/agent_network/components/tool_invocations/terminal_output_renderer.dart

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class TerminalOutputRenderer extends StatefulComponent {
2727

2828
class _TerminalOutputRendererState extends State<TerminalOutputRenderer> {
2929
bool isExpanded = false;
30+
bool isHovered = false;
31+
final ScrollController _scrollController = ScrollController();
3032

3133
/// Regex to match ANSI escape sequences (color codes, etc.)
3234
static final _ansiRegex = RegExp(r'\x1b\[[0-9;]*m');
@@ -88,18 +90,22 @@ class _TerminalOutputRendererState extends State<TerminalOutputRenderer> {
8890
);
8991
}
9092

91-
return GestureDetector(
92-
onTap: () => setState(() => isExpanded = !isExpanded),
93-
child: Container(
94-
padding: EdgeInsets.only(bottom: 1),
95-
child: Column(
96-
crossAxisAlignment: CrossAxisAlignment.start,
97-
children: [
98-
// Tool header with name and params
99-
_buildHeader(),
100-
// Terminal output
101-
_buildOutput(lines),
102-
],
93+
return MouseRegion(
94+
onEnter: (_) => setState(() => isHovered = true),
95+
onExit: (_) => setState(() => isHovered = false),
96+
child: GestureDetector(
97+
onTap: () => setState(() => isExpanded = !isExpanded),
98+
child: Container(
99+
padding: EdgeInsets.only(bottom: 1),
100+
child: Column(
101+
crossAxisAlignment: CrossAxisAlignment.start,
102+
children: [
103+
// Tool header with name and params
104+
_buildHeader(),
105+
// Terminal output
106+
_buildOutput(lines),
107+
],
108+
),
103109
),
104110
),
105111
);
@@ -148,7 +154,7 @@ class _TerminalOutputRendererState extends State<TerminalOutputRenderer> {
148154

149155
return Container(
150156
decoration: BoxDecoration(
151-
color: Color(0xFF1E1E1E), // Dark terminal background
157+
color: isHovered ? Color(0xFF222222) : Color(0xFF1E1E1E),
152158
),
153159
padding: EdgeInsets.all(1),
154160
child: Column(
@@ -159,10 +165,17 @@ class _TerminalOutputRendererState extends State<TerminalOutputRenderer> {
159165
// Scrollable container for expanded state with many lines
160166
Container(
161167
constraints: BoxConstraints(maxHeight: 8),
162-
child: SingleChildScrollView(
163-
child: Column(
164-
crossAxisAlignment: CrossAxisAlignment.start,
165-
children: [for (final line in displayLines) _buildLine(line)],
168+
child: Scrollbar(
169+
controller: _scrollController,
170+
thumbVisibility: true,
171+
thumbColor: Colors.white.withOpacity(0.3),
172+
trackColor: Color(0xFF1E1E1E),
173+
child: SingleChildScrollView(
174+
controller: _scrollController,
175+
child: Column(
176+
crossAxisAlignment: CrossAxisAlignment.start,
177+
children: [for (final line in displayLines) _buildLine(line)],
178+
),
166179
),
167180
),
168181
)

packages/vide_in_app_sdk/lib/src/state/sdk_state.dart

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class VideSdkState extends ChangeNotifier {
2626
VideSdkConnectionState _connectionState = VideSdkConnectionState.disconnected;
2727
String? _errorMessage;
2828
StreamSubscription<VideEvent>? _eventSubscription;
29+
PlanApprovalRequestEvent? _pendingPlanApproval;
30+
AskUserQuestionEvent? _pendingAskUserQuestion;
31+
final List<PermissionRequestEvent> _pendingPermissions = [];
2932

3033
VideSdkState({String? host, int? port, String? workingDirectory})
3134
: _host = host,
@@ -44,18 +47,96 @@ class VideSdkState extends ChangeNotifier {
4447

4548
VideSdkConnectionState get connectionState => _connectionState;
4649
String? get errorMessage => _errorMessage;
50+
51+
/// A [VideClient] for API calls (filesystem, git, etc.).
52+
///
53+
/// Available whenever host and port are configured, regardless of session
54+
/// state.
55+
VideClient? get client =>
56+
_host != null && _port != null ? VideClient(host: _host!, port: _port!) : null;
4757
RemoteVideSession? get session => _session;
4858
VideState? get videState => _session?.state;
4959
bool get hasActiveSession =>
5060
_session != null && _connectionState == VideSdkConnectionState.connected;
5161

62+
PlanApprovalRequestEvent? get pendingPlanApproval => _pendingPlanApproval;
63+
AskUserQuestionEvent? get pendingAskUserQuestion => _pendingAskUserQuestion;
64+
PermissionRequestEvent? get currentPermission => _pendingPermissions.firstOrNull;
65+
66+
void dequeuePermission() {
67+
if (_pendingPermissions.isNotEmpty) {
68+
_pendingPermissions.removeAt(0);
69+
notifyListeners();
70+
}
71+
}
72+
73+
void _removePermissionByRequestId(String requestId) {
74+
_pendingPermissions.removeWhere((r) => r.requestId == requestId);
75+
notifyListeners();
76+
}
77+
78+
/// Whether the server health check has passed (server is reachable).
79+
/// null = not checked yet, true = reachable, false = unreachable.
80+
bool? _serverReachable;
81+
bool? get serverReachable => _serverReachable;
82+
83+
/// Sessions from the server filtered to the configured working directory.
84+
List<SessionSummary> _sessions = [];
85+
List<SessionSummary> get sessions => _sessions;
86+
bool _sessionsFetched = false;
87+
bool get sessionsFetched => _sessionsFetched;
88+
5289
/// Load persisted configuration from shared preferences.
90+
///
91+
/// Also kicks off a background health check if configured.
5392
Future<void> loadConfig() async {
5493
final prefs = await SharedPreferences.getInstance();
5594
_host ??= prefs.getString(_kHostKey);
5695
_port ??= prefs.getInt(_kPortKey);
5796
_workingDirectory ??= prefs.getString(_kWorkingDirKey);
5897
notifyListeners();
98+
99+
if (isConfigured) {
100+
unawaited(checkServerHealth());
101+
}
102+
}
103+
104+
/// Check if the configured server is reachable.
105+
///
106+
/// If reachable, also fetches the session list for the working directory.
107+
Future<void> checkServerHealth() async {
108+
if (_host == null || _port == null) return;
109+
110+
_serverReachable = null;
111+
notifyListeners();
112+
113+
final reachable = await testConnection(host: _host!, port: _port!);
114+
_serverReachable = reachable;
115+
notifyListeners();
116+
117+
if (reachable) {
118+
unawaited(fetchSessions());
119+
}
120+
}
121+
122+
/// Fetch sessions from the server filtered to the configured working directory.
123+
Future<void> fetchSessions() async {
124+
if (_host == null || _port == null || _workingDirectory == null) return;
125+
126+
final client = VideClient(host: _host!, port: _port!);
127+
try {
128+
final all = await client.listSessions();
129+
_sessions = all
130+
.where((s) =>
131+
s.workingDirectory == _workingDirectory &&
132+
s.state == 'ready')
133+
.toList()
134+
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
135+
_sessionsFetched = true;
136+
notifyListeners();
137+
} catch (_) {
138+
// Non-critical — leave list empty
139+
}
59140
}
60141

61142
/// Update and persist configuration.
@@ -75,7 +156,10 @@ class VideSdkState extends ChangeNotifier {
75156

76157
// Disconnect any existing session since config changed
77158
await disconnect();
159+
_serverReachable = null;
78160
notifyListeners();
161+
162+
unawaited(checkServerHealth());
79163
}
80164

81165
/// Test connection to a server by checking its health endpoint.
@@ -147,7 +231,28 @@ class VideSdkState extends ChangeNotifier {
147231

148232
void _setupEventListening() {
149233
_eventSubscription?.cancel();
150-
_eventSubscription = _session?.events.listen((_) {
234+
_eventSubscription = _session?.events.listen((event) {
235+
switch (event) {
236+
case PermissionRequestEvent():
237+
_pendingPermissions.add(event);
238+
case PermissionResolvedEvent(:final requestId):
239+
_removePermissionByRequestId(requestId);
240+
return; // already notified in helper
241+
case PlanApprovalRequestEvent():
242+
_pendingPlanApproval = event;
243+
case PlanApprovalResolvedEvent(:final requestId):
244+
if (_pendingPlanApproval?.requestId == requestId) {
245+
_pendingPlanApproval = null;
246+
}
247+
case AskUserQuestionEvent():
248+
_pendingAskUserQuestion = event;
249+
case AskUserQuestionResolvedEvent(:final requestId):
250+
if (_pendingAskUserQuestion?.requestId == requestId) {
251+
_pendingAskUserQuestion = null;
252+
}
253+
default:
254+
break;
255+
}
151256
notifyListeners();
152257
});
153258

@@ -163,8 +268,12 @@ class VideSdkState extends ChangeNotifier {
163268
}
164269

165270
/// Respond to a permission request.
166-
void respondToPermission(String requestId, {required bool allow}) {
167-
_session?.respondToPermission(requestId, allow: allow);
271+
void respondToPermission(
272+
String requestId, {
273+
required bool allow,
274+
bool remember = false,
275+
}) {
276+
_session?.respondToPermission(requestId, allow: allow, remember: remember);
168277
}
169278

170279
/// Respond to a plan approval request.
@@ -180,6 +289,14 @@ class VideSdkState extends ChangeNotifier {
180289
);
181290
}
182291

292+
/// Respond to an AskUserQuestion request.
293+
void respondToAskUserQuestion(
294+
String requestId, {
295+
required Map<String, String> answers,
296+
}) {
297+
_session?.respondToAskUserQuestion(requestId, answers: answers);
298+
}
299+
183300
/// Abort the current session.
184301
Future<void> abort() async {
185302
await _session?.abort();
@@ -194,7 +311,13 @@ class VideSdkState extends ChangeNotifier {
194311
_client = null;
195312
_connectionState = VideSdkConnectionState.disconnected;
196313
_errorMessage = null;
314+
_pendingPlanApproval = null;
315+
_pendingAskUserQuestion = null;
316+
_pendingPermissions.clear();
197317
notifyListeners();
318+
319+
// Refresh session list so the disconnected session appears under "Recent".
320+
unawaited(fetchSessions());
198321
}
199322

200323
@override

0 commit comments

Comments
 (0)