Skip to content

Commit 23a93f4

Browse files
committed
Decouple NetworkExecutionPage from global session providers
Thread VideSession directly through NetworkExecutionPage and _AgentChat as a constructor parameter, eliminating 14 reads of the global currentVideSessionProvider. Convert selectedAgentIdProvider and currentModelProvider to family providers keyed by session ID. This removes unnecessary global coupling from the TUI layer, preparing for multi-team support.
1 parent 82e82fd commit 23a93f4

File tree

4 files changed

+63
-73
lines changed

4 files changed

+63
-73
lines changed

lib/components/vide_scaffold.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,16 @@ class _VideScaffoldState extends State<VideScaffold> {
200200
minWidth: component.sidebarWidth - 1,
201201
maxWidth: component.sidebarWidth - 1,
202202
child: AgentSidebar(
203+
sessionId: context.read(sessionSelectionProvider).sessionId ?? '',
203204
width: (component.sidebarWidth - 1).toInt(),
204205
focused: focused,
205206
expanded: true,
206207
onExitRight: () {
207208
setState(() => _focusedPanel = FocusedPanel.content);
208209
},
209210
onSelectAgent: (agentId) {
210-
context.read(selectedAgentIdProvider.notifier).state =
211+
final sessionId = context.read(sessionSelectionProvider).sessionId ?? '';
212+
context.read(selectedAgentIdProvider(sessionId).notifier).state =
211213
agentId;
212214
setState(() => _focusedPanel = FocusedPanel.content);
213215
},
@@ -253,7 +255,8 @@ class _VideScaffoldState extends State<VideScaffold> {
253255
setState(() => _focusedPanel = FocusedPanel.content);
254256
},
255257
onSendMessage: (message) {
256-
final selectedAgentId = context.read(selectedAgentIdProvider);
258+
final sessionId = context.read(sessionSelectionProvider).sessionId ?? '';
259+
final selectedAgentId = context.read(selectedAgentIdProvider(sessionId));
257260
if (selectedAgentId != null) {
258261
session?.sendMessage(
259262
AgentMessage(text: message),
@@ -284,7 +287,8 @@ class _VideScaffoldState extends State<VideScaffold> {
284287
final goalText = goalAsync.valueOrNull ?? session?.state.goal ?? 'Session';
285288
final primary = theme.base.primary;
286289
final dimmer = theme.base.onSurface.withOpacity(TextOpacity.tertiary);
287-
final model = context.watch(currentModelProvider);
290+
final sessionId = context.read(sessionSelectionProvider).sessionId ?? '';
291+
final model = context.watch(currentModelProvider(sessionId));
288292
return Container(
289293
decoration: BoxDecoration(
290294
border: BoxBorder(bottom: BorderSide(color: theme.base.outlineVariant)),

lib/modules/agent_network/components/agent_sidebar.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import 'package:vide_cli/modules/agent_network/state/vide_session_providers.dart
1212
/// - Enter/Space: Select agent
1313
/// - Escape or Right Arrow: Exit sidebar
1414
class AgentSidebar extends StatefulComponent {
15+
final String sessionId;
1516
final bool focused;
1617
final bool expanded;
1718
final int width;
1819
final VoidCallback? onExitRight;
1920
final void Function(String agentId)? onSelectAgent;
2021

2122
const AgentSidebar({
23+
required this.sessionId,
2224
required this.focused,
2325
required this.expanded,
2426
this.width = 50,
@@ -191,7 +193,7 @@ class _AgentSidebarState extends State<AgentSidebar>
191193
final item = items[_selectedIndex];
192194
if (item.agent != null) {
193195
// Select spawned agent - just update the provider, keep focus in sidebar
194-
context.read(selectedAgentIdProvider.notifier).state = item.agent!.id;
196+
context.read(selectedAgentIdProvider(component.sessionId).notifier).state = item.agent!.id;
195197
// Note: We intentionally don't call onSelectAgent here to keep focus in sidebar
196198
}
197199
}
@@ -212,11 +214,11 @@ class _AgentSidebarState extends State<AgentSidebar>
212214
agentsAsync.valueOrNull ?? session?.state.agents ?? [];
213215

214216
// Auto-select first agent if none selected
215-
final currentSelectedId = context.read(selectedAgentIdProvider);
217+
final currentSelectedId = context.read(selectedAgentIdProvider(component.sessionId));
216218
if (currentSelectedId == null && spawnedAgents.isNotEmpty) {
217219
// Schedule the state update for after build
218220
Future.microtask(() {
219-
context.read(selectedAgentIdProvider.notifier).state =
221+
context.read(selectedAgentIdProvider(component.sessionId).notifier).state =
220222
spawnedAgents.first.id;
221223
});
222224
}
@@ -274,7 +276,7 @@ class _AgentSidebarState extends State<AgentSidebar>
274276
VideThemeData theme,
275277
List<VideAgent> spawnedAgents,
276278
) {
277-
final selectedAgentId = context.watch(selectedAgentIdProvider);
279+
final selectedAgentId = context.watch(selectedAgentIdProvider(component.sessionId));
278280

279281
// Build items
280282
final items = _buildItems(spawnedAgents);
@@ -360,7 +362,7 @@ class _AgentSidebarState extends State<AgentSidebar>
360362
onHoverExit: () => setState(() => _hoveredIndex = null),
361363
onTap: () {
362364
setState(() => _selectedIndex = index);
363-
context.read(selectedAgentIdProvider.notifier).state = agent.id;
365+
context.read(selectedAgentIdProvider(component.sessionId).notifier).state = agent.id;
364366
component.onSelectAgent?.call(agent.id);
365367
},
366368
);

lib/modules/agent_network/network_execution_page.dart

Lines changed: 47 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import 'package:vide_cli/theme/theme.dart';
2626
import 'package:vide_cli/components/vide_scaffold.dart';
2727

2828
class NetworkExecutionPage extends StatefulComponent {
29-
const NetworkExecutionPage({super.key});
29+
final VideSession session;
30+
31+
const NetworkExecutionPage({required this.session, super.key});
3032

3133
static Future<void> push(BuildContext context, {required VideSession session}) async {
3234
context.read(sessionSelectionProvider.notifier).selectSession(session);
@@ -39,7 +41,7 @@ class NetworkExecutionPage extends StatefulComponent {
3941

4042
return Navigator.of(
4143
context,
42-
).push<void>(PageRoute(builder: (context) => const NetworkExecutionPage(), settings: RouteSettings()));
44+
).push<void>(PageRoute(builder: (context) => NetworkExecutionPage(session: session), settings: RouteSettings()));
4345
}
4446

4547
@override
@@ -104,8 +106,9 @@ class _NetworkExecutionPageState extends State<NetworkExecutionPage> {
104106
required VoidCallback focusRightSidebar,
105107
}) {
106108
// Get selected agent ID from provider, or use the first agent
107-
final selectedAgentIdNotifier = context.read(selectedAgentIdProvider.notifier);
108-
final selectedAgentId = context.watch(selectedAgentIdProvider);
109+
final sessionId = component.session.id;
110+
final selectedAgentIdNotifier = context.read(selectedAgentIdProvider(sessionId).notifier);
111+
final selectedAgentId = context.watch(selectedAgentIdProvider(sessionId));
109112

110113
// Find the selected agent, or default to the first agent
111114
String agentId = selectedAgentId ?? (agentIds.isNotEmpty ? agentIds[0] : '');
@@ -116,10 +119,11 @@ class _NetworkExecutionPageState extends State<NetworkExecutionPage> {
116119
selectedAgentIdNotifier.state = agentId;
117120
}
118121

119-
final session = context.read(currentVideSessionProvider)!;
122+
final session = component.session;
120123
return Expanded(
121124
child: _AgentChat(
122125
key: ValueKey(agentId),
126+
session: session,
123127
agentId: agentId,
124128
networkId: session.id,
125129
showQuitWarning: _showQuitWarning,
@@ -132,19 +136,16 @@ class _NetworkExecutionPageState extends State<NetworkExecutionPage> {
132136
}
133137

134138
Future<void> _exitWithDaemonCleanup() async {
135-
final session = context.read(currentVideSessionProvider);
136-
final sessionId = session?.id;
137-
if (sessionId != null) {
138-
final daemonState = context.read(daemonConnectionProvider);
139-
if (daemonState.isConnected) {
140-
try {
141-
await context
142-
.read(daemonConnectionProvider.notifier)
143-
.stopSession(sessionId)
144-
.timeout(const Duration(seconds: 3));
145-
} catch (_) {
146-
// Best-effort — don't block exit if stop fails or times out.
147-
}
139+
final sessionId = component.session.id;
140+
final daemonState = context.read(daemonConnectionProvider);
141+
if (daemonState.isConnected) {
142+
try {
143+
await context
144+
.read(daemonConnectionProvider.notifier)
145+
.stopSession(sessionId)
146+
.timeout(const Duration(seconds: 3));
147+
} catch (_) {
148+
// Best-effort — don't block exit if stop fails or times out.
148149
}
149150
}
150151
shutdownApp();
@@ -181,9 +182,9 @@ class _NetworkExecutionPageState extends State<NetworkExecutionPage> {
181182
// Remote sessions start disconnected while the WebSocket connects.
182183
final connectionAsync = context.watch(sessionConnectionProvider);
183184
final isConnected = connectionAsync.valueOrNull ?? true;
184-
final session = context.watch(currentVideSessionProvider);
185+
final session = component.session;
185186

186-
if (session == null || !isConnected) {
187+
if (!isConnected) {
187188
return _buildConnectingScreen(context, label: 'Connecting to session...');
188189
}
189190

@@ -260,6 +261,7 @@ class _NetworkExecutionPageState extends State<NetworkExecutionPage> {
260261
}
261262

262263
class _AgentChat extends StatefulComponent {
264+
final VideSession session;
263265
final String agentId;
264266
final String networkId;
265267
final bool showQuitWarning;
@@ -269,6 +271,7 @@ class _AgentChat extends StatefulComponent {
269271
final VoidCallback focusRightSidebar;
270272

271273
const _AgentChat({
274+
required this.session,
272275
required this.agentId,
273276
required this.networkId,
274277
required this.onExit,
@@ -309,15 +312,14 @@ class _AgentChatState extends State<_AgentChat> {
309312
_queuedMessage = queuedMessage;
310313
_model = model;
311314
});
312-
context.read(currentModelProvider.notifier).state = model;
315+
context.read(currentModelProvider(component.session.id).notifier).state = model;
313316
}
314317

315318
@override
316319
void initState() {
317320
super.initState();
318321

319-
final session = context.read(currentVideSessionProvider);
320-
if (session == null) return;
322+
final session = component.session;
321323

322324
// Listen to conversation updates
323325
_conversationSubscription = session.conversationStream(component.agentId).listen((conversation) {
@@ -338,7 +340,7 @@ class _AgentChatState extends State<_AgentChat> {
338340
// Listen to model updates
339341
_modelSubscription = session.modelStream(component.agentId).listen((model) {
340342
setState(() => _model = model);
341-
context.read(currentModelProvider.notifier).state = model;
343+
context.read(currentModelProvider(component.session.id).notifier).state = model;
342344
});
343345

344346
unawaited(_loadInitialAgentRuntimeMetadata(session));
@@ -367,40 +369,37 @@ class _AgentChatState extends State<_AgentChat> {
367369
if (message.attachments != null && message.attachments!.isNotEmpty) {
368370
_sentAttachments[message.text] = message.attachments!;
369371
}
370-
final session = context.read(currentVideSessionProvider);
371-
session?.sendMessage(message, agentId: component.agentId);
372+
component.session.sendMessage(message, agentId: component.agentId);
372373
}
373374

374375
bool _isLastAgent() {
375-
final session = context.read(currentVideSessionProvider);
376-
if (session == null) return true; // If no session, treat as last agent (safe default)
377-
return session.state.agents.length <= 1;
376+
return component.session.state.agents.length <= 1;
378377
}
379378

380379
Future<void> _handleCommand(String commandInput) async {
381-
final session = context.read(currentVideSessionProvider);
380+
final session = component.session;
382381
final dispatcher = context.read(commandDispatcherProvider);
383382
final commandContext = CommandContext(
384383
agentId: component.agentId,
385-
workingDirectory: session?.state.workingDirectory ?? '',
384+
workingDirectory: session.state.workingDirectory,
386385
isLastAgent: _isLastAgent(),
387386
sendMessage: (message) {
388-
session?.sendMessage(AgentMessage(text: message), agentId: component.agentId);
387+
session.sendMessage(AgentMessage(text: message), agentId: component.agentId);
389388
},
390389
clearConversation: () async {
391-
await session?.clearConversation(agentId: component.agentId);
390+
await session.clearConversation(agentId: component.agentId);
392391
setState(() {
393392
_conversation = null;
394393
});
395394
},
396395
exitApp: component.onExit,
397396
detachApp: shutdownApp,
398397
forkAgent: (name) async {
399-
final newAgentId = await session?.forkAgent(component.agentId, name: name);
400-
return newAgentId ?? '';
398+
final newAgentId = await session.forkAgent(component.agentId, name: name);
399+
return newAgentId;
401400
},
402401
killAgent: () async {
403-
await session?.terminateAgent(
402+
await session.terminateAgent(
404403
component.agentId,
405404
terminatedBy: component.agentId, // Self-termination
406405
reason: 'User invoked /kill command',
@@ -413,22 +412,21 @@ class _AgentChatState extends State<_AgentChat> {
413412
context,
414413
repoPath: repoPath,
415414
onSendMessage: (message) {
416-
session?.sendMessage(AgentMessage(text: message), agentId: component.agentId);
415+
session.sendMessage(AgentMessage(text: message), agentId: component.agentId);
417416
},
418417
onSwitchWorktree: (path) async {
419418
final container = ProviderScope.containerOf(context);
420419
container.read(repoPathOverrideProvider.notifier).state = path;
421420
// Use VideSession.setWorktreePath() instead of direct provider access
422-
await session?.setWorktreePath(path);
421+
await session.setWorktreePath(path);
423422
},
424423
);
425424
},
426425
showSettingsDialog: () async {
427426
await SettingsPopup.show(context);
428427
},
429428
showSessionLogs: () {
430-
final sessionId = session?.id;
431-
if (sessionId == null) return;
429+
final sessionId = session.id;
432430
final logPath = VideLogger.instance.sessionLogPath(sessionId);
433431
context.read(filePreviewPathProvider.notifier).state = logPath;
434432
},
@@ -467,9 +465,8 @@ class _AgentChatState extends State<_AgentChat> {
467465
}
468466

469467
Future<List<CommandSuggestion>> _getFileSuggestions(String query) async {
470-
final session = context.read(currentVideSessionProvider);
471-
final workingDir = session?.state.workingDirectory;
472-
if (workingDir == null) return [];
468+
final workingDir = component.session.state.workingDirectory;
469+
if (workingDir.isEmpty) return [];
473470

474471
final now = DateTime.now();
475472
if (_cachedFileList == null ||
@@ -517,8 +514,6 @@ class _AgentChatState extends State<_AgentChat> {
517514
String? patternOverride,
518515
String? denyReason,
519516
}) {
520-
final session = context.read(currentVideSessionProvider);
521-
522517
String reason;
523518
if (granted) {
524519
reason = 'User approved';
@@ -528,7 +523,7 @@ class _AgentChatState extends State<_AgentChat> {
528523
reason = 'User denied';
529524
}
530525

531-
session?.respondToPermission(
526+
component.session.respondToPermission(
532527
request.requestId,
533528
allow: granted,
534529
message: reason,
@@ -541,34 +536,27 @@ class _AgentChatState extends State<_AgentChat> {
541536
}
542537

543538
void _handleAskUserQuestionResponse(AskUserQuestionUIRequest request, Map<String, String> answers) {
544-
final session = context.read(currentVideSessionProvider);
545-
546539
// Send the response through the session (unified path for local and remote)
547-
session?.respondToAskUserQuestion(request.requestId, answers: answers);
540+
component.session.respondToAskUserQuestion(request.requestId, answers: answers);
548541

549542
// Dequeue the current request to show the next one
550543
context.read(askUserQuestionStateProvider.notifier).dequeueRequest();
551544
}
552545

553546
void _handlePlanApprovalResponse(PlanApprovalUIRequest request, String action, String? feedback) {
554-
final session = context.read(currentVideSessionProvider);
555-
556-
session?.respondToPlanApproval(request.requestId, action: action, feedback: feedback);
547+
component.session.respondToPlanApproval(request.requestId, action: action, feedback: feedback);
557548

558549
// Dequeue the current request to show the next one
559550
context.read(planApprovalStateProvider.notifier).dequeueRequest();
560551
}
561552

562553
void _handleEscape() {
563-
final session = context.read(currentVideSessionProvider);
564-
if (session == null) return;
565-
566554
// If there's a queued message, clear it first
567555
if (_queuedMessage != null) {
568-
unawaited(session.clearQueuedMessage(component.agentId));
556+
unawaited(component.session.clearQueuedMessage(component.agentId));
569557
} else {
570558
// Otherwise abort the current processing
571-
session.abortAgent(component.agentId);
559+
component.session.abortAgent(component.agentId);
572560
}
573561
}
574562

@@ -624,8 +612,7 @@ class _AgentChatState extends State<_AgentChat> {
624612
itemBuilder: (context, index) {
625613
final messageIndex = index;
626614
final message = filteredMessages[messageIndex];
627-
final session = context.read(currentVideSessionProvider);
628-
final workingDir = session?.state.workingDirectory ?? '';
615+
final workingDir = component.session.state.workingDirectory;
629616

630617
// Render system-like user messages (e.g. "[Request interrupted by user]")
631618
// as dimmed inline text instead of a full user message bubble.
@@ -727,10 +714,7 @@ class _AgentChatState extends State<_AgentChat> {
727714
model: _model,
728715
maxDialogHeight: maxDialogHeight,
729716
onClearQueue: () {
730-
final session = context.read(currentVideSessionProvider);
731-
if (session != null) {
732-
unawaited(session.clearQueuedMessage(component.agentId));
733-
}
717+
unawaited(component.session.clearQueuedMessage(component.agentId));
734718
},
735719
onSendMessage: _sendMessage,
736720
onCommand: _handleCommand,

0 commit comments

Comments
 (0)