Skip to content

Commit 399f3f0

Browse files
committed
feat(mobile): improve session context binding across screens
Wire session context to Git and repository screens using current chat session fallback. Add sessionId query parameter support for deep linking. Improve payload normalization for file listings and handle nullable branch metadata. - Add sessionId query parameter to GitScreen route - Use resolvedSessionIdProvider for context-fallback in Git and file screens - Sync current session state in ChatScreen lifecycle - Add normalizeFileListPayload for bridge payload compatibility - Handle nullable branch in session metadata - Add tests for git screen context binding and file tree screens
1 parent 8d15833 commit 399f3f0

12 files changed

Lines changed: 750 additions & 173 deletions

File tree

apps/mobile/lib/core/config/router.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ GoRouter _buildRouter() {
7878
routes: [
7979
GoRoute(
8080
path: '/home/git',
81-
builder: (_, __) => const GitScreen(sessionId: ''),
81+
builder: (_, state) {
82+
final sessionId =
83+
state.uri.queryParameters['sessionId'] ?? '';
84+
return GitScreen(sessionId: sessionId);
85+
},
8286
),
8387
],
8488
),

apps/mobile/lib/features/chat/presentation/screens/chat_screen.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,33 @@ class ChatScreen extends ConsumerStatefulWidget {
2020
class _ChatScreenState extends ConsumerState<ChatScreen> {
2121
final _scrollController = ScrollController();
2222

23+
@override
24+
void initState() {
25+
super.initState();
26+
_syncCurrentSession();
27+
}
28+
29+
@override
30+
void didUpdateWidget(covariant ChatScreen oldWidget) {
31+
super.didUpdateWidget(oldWidget);
32+
if (oldWidget.sessionId != widget.sessionId) {
33+
_syncCurrentSession();
34+
}
35+
}
36+
2337
@override
2438
void dispose() {
2539
_scrollController.dispose();
2640
super.dispose();
2741
}
2842

43+
void _syncCurrentSession() {
44+
final notifier = ref.read(currentSessionProvider.notifier);
45+
if (notifier.state != widget.sessionId) {
46+
notifier.state = widget.sessionId;
47+
}
48+
}
49+
2950
void _scrollToBottom() {
3051
WidgetsBinding.instance.addPostFrameCallback((_) {
3152
if (_scrollController.hasClients) {

apps/mobile/lib/features/chat/presentation/screens/session_list_screen.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class SessionListScreen extends ConsumerWidget {
1414

1515
@override
1616
Widget build(BuildContext context, WidgetRef ref) {
17-
ref.watch(chatNotifierProvider);
1817
final sessionsAsync = ref.watch(activeSessionsProvider);
1918

2019
return Scaffold(

apps/mobile/lib/features/git/presentation/screens/git_screen.dart

Lines changed: 163 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_riverpod/flutter_riverpod.dart';
35
import 'package:go_router/go_router.dart';
46

7+
import '../../../chat/domain/providers/session_provider.dart';
58
import '../../domain/providers/git_provider.dart';
69
import '../widgets/file_change_tile.dart';
710
import '../widgets/git_status_card.dart';
811

912
/// Main Git screen showing repository status and a list of changed files.
1013
///
11-
/// Requires a [sessionId] to scope WS requests. The session ID is taken from
12-
/// the `extra` field of the route or falls back to an empty string for
13-
/// demonstration purposes when navigated from the bottom nav.
14+
/// Uses the explicit [sessionId] when provided, otherwise falls back to the
15+
/// currently selected chat session.
1416
class GitScreen extends ConsumerStatefulWidget {
1517
final String sessionId;
1618

@@ -21,117 +23,197 @@ class GitScreen extends ConsumerStatefulWidget {
2123
}
2224

2325
class _GitScreenState extends ConsumerState<GitScreen> {
26+
ProviderSubscription<String?>? _sessionIdSubscription;
27+
2428
@override
2529
void initState() {
2630
super.initState();
27-
if (widget.sessionId.isNotEmpty) {
28-
WidgetsBinding.instance.addPostFrameCallback((_) {
29-
ref
30-
.read(gitStatusProvider(widget.sessionId).notifier)
31-
.fetchStatus(widget.sessionId);
32-
});
31+
_bindSessionContext();
32+
}
33+
34+
@override
35+
void didUpdateWidget(covariant GitScreen oldWidget) {
36+
super.didUpdateWidget(oldWidget);
37+
if (oldWidget.sessionId != widget.sessionId) {
38+
_bindSessionContext();
3339
}
3440
}
3541

42+
@override
43+
void dispose() {
44+
_sessionIdSubscription?.close();
45+
super.dispose();
46+
}
47+
48+
void _bindSessionContext() {
49+
_sessionIdSubscription?.close();
50+
_sessionIdSubscription = ref.listenManual<String?>(
51+
resolvedSessionIdProvider(widget.sessionId),
52+
(previous, next) {
53+
if (next == null || next.isEmpty || next == previous) {
54+
return;
55+
}
56+
57+
Future<void>.microtask(
58+
() => ref.read(gitStatusProvider(next).notifier).fetchStatus(next),
59+
);
60+
},
61+
fireImmediately: true,
62+
);
63+
}
64+
65+
String? _resolvedSessionId() {
66+
return ref.read(resolvedSessionIdProvider(widget.sessionId));
67+
}
68+
3669
Future<void> _refresh() async {
37-
if (widget.sessionId.isNotEmpty) {
38-
await ref
39-
.read(gitStatusProvider(widget.sessionId).notifier)
40-
.fetchStatus(widget.sessionId);
70+
final sessionId = _resolvedSessionId();
71+
if (sessionId == null) {
72+
return;
4173
}
74+
75+
await ref
76+
.read(gitStatusProvider(sessionId).notifier)
77+
.fetchStatus(sessionId);
4278
}
4379

4480
void _onPull() {
45-
if (widget.sessionId.isNotEmpty) {
46-
ref
47-
.read(gitStatusProvider(widget.sessionId).notifier)
48-
.pull(widget.sessionId);
81+
final sessionId = _resolvedSessionId();
82+
if (sessionId == null) {
83+
return;
4984
}
85+
86+
ref.read(gitStatusProvider(sessionId).notifier).pull(sessionId);
5087
}
5188

5289
void _onPush() {
53-
if (widget.sessionId.isNotEmpty) {
54-
ref
55-
.read(gitStatusProvider(widget.sessionId).notifier)
56-
.push(widget.sessionId);
90+
final sessionId = _resolvedSessionId();
91+
if (sessionId == null) {
92+
return;
5793
}
94+
95+
ref.read(gitStatusProvider(sessionId).notifier).push(sessionId);
5896
}
5997

6098
void _onCommit(List changes) {
99+
final sessionId = _resolvedSessionId();
100+
if (sessionId == null) {
101+
return;
102+
}
103+
61104
context.push(
62105
'/git/commit',
63-
extra: {'sessionId': widget.sessionId, 'changes': changes},
106+
extra: {'sessionId': sessionId, 'changes': changes},
64107
);
65108
}
66109

67110
@override
68111
Widget build(BuildContext context) {
69-
final statusAsync = widget.sessionId.isNotEmpty
70-
? ref.watch(gitStatusProvider(widget.sessionId))
112+
final resolvedSessionId =
113+
ref.watch(resolvedSessionIdProvider(widget.sessionId));
114+
final statusAsync = resolvedSessionId != null
115+
? ref.watch(gitStatusProvider(resolvedSessionId))
71116
: const AsyncValue<dynamic>.data(null);
72117

73118
return Scaffold(
74119
appBar: AppBar(title: const Text('Git')),
75-
body: RefreshIndicator(
76-
onRefresh: _refresh,
77-
child: statusAsync.when(
78-
loading: () => const Center(child: CircularProgressIndicator()),
79-
error: (e, _) => Center(child: Text('Error: $e')),
80-
data: (status) {
81-
if (status == null) {
82-
return _emptyState();
83-
}
84-
85-
if (status.isClean) {
86-
return Column(
87-
children: [
88-
GitStatusCard(status: status),
89-
Expanded(child: _emptyState()),
90-
],
91-
);
92-
}
93-
94-
return Column(
95-
children: [
96-
GitStatusCard(status: status),
97-
const Divider(height: 1),
98-
Expanded(
99-
child: ListView.builder(
100-
itemCount: status.changes.length,
101-
itemBuilder: (context, index) {
102-
final change = status.changes[index];
103-
return FileChangeTile(
104-
change: change,
105-
onTap: () {},
106-
);
107-
},
108-
),
109-
),
110-
_ActionRow(
111-
onPull: _onPull,
112-
onCommit: () => _onCommit(status.changes),
113-
onPush: _onPush,
114-
),
115-
],
116-
);
117-
},
118-
),
119-
),
120+
body: resolvedSessionId == null
121+
? _sessionRequiredState()
122+
: RefreshIndicator(
123+
onRefresh: _refresh,
124+
child: statusAsync.when(
125+
loading: () => const Center(child: CircularProgressIndicator()),
126+
error: (e, _) => Center(child: Text('Error: $e')),
127+
data: (status) {
128+
if (status == null) {
129+
return _placeholderState(
130+
icon: Icons.sync_outlined,
131+
title: 'Awaiting git status',
132+
subtitle:
133+
'Pull to refresh if the repository summary does not appear.',
134+
);
135+
}
136+
137+
if (status.isClean) {
138+
return Column(
139+
children: [
140+
GitStatusCard(status: status),
141+
Expanded(
142+
child: _placeholderState(
143+
icon: Icons.check_circle_outline,
144+
title: 'Repository is clean',
145+
subtitle:
146+
'No working tree changes were reported for this session.',
147+
),
148+
),
149+
],
150+
);
151+
}
152+
153+
return Column(
154+
children: [
155+
GitStatusCard(status: status),
156+
const Divider(height: 1),
157+
Expanded(
158+
child: ListView.builder(
159+
itemCount: status.changes.length,
160+
itemBuilder: (context, index) {
161+
final change = status.changes[index];
162+
return FileChangeTile(
163+
change: change,
164+
onTap: () {},
165+
);
166+
},
167+
),
168+
),
169+
_ActionRow(
170+
onPull: _onPull,
171+
onCommit: () => _onCommit(status.changes),
172+
onPush: _onPush,
173+
),
174+
],
175+
);
176+
},
177+
),
178+
),
120179
);
121180
}
122181

123-
Widget _emptyState() {
124-
return const Center(
125-
child: Column(
126-
mainAxisSize: MainAxisSize.min,
127-
children: [
128-
Icon(Icons.check_circle_outline, size: 48, color: Color(0xFF4CAF50)),
129-
SizedBox(height: 12),
130-
Text(
131-
'Repository is clean',
132-
style: TextStyle(fontSize: 15, color: Color(0xFF9E9E9E)),
133-
),
134-
],
182+
Widget _sessionRequiredState() {
183+
return _placeholderState(
184+
icon: Icons.source_outlined,
185+
title: 'Select a session first',
186+
subtitle:
187+
'Open a Claude session in Chat to inspect repository status for that workspace.',
188+
);
189+
}
190+
191+
Widget _placeholderState({
192+
required IconData icon,
193+
required String title,
194+
required String subtitle,
195+
}) {
196+
return Center(
197+
child: Padding(
198+
padding: const EdgeInsets.symmetric(horizontal: 24),
199+
child: Column(
200+
mainAxisSize: MainAxisSize.min,
201+
children: [
202+
Icon(icon, size: 48, color: const Color(0xFF9E9E9E)),
203+
const SizedBox(height: 12),
204+
Text(
205+
title,
206+
textAlign: TextAlign.center,
207+
style: const TextStyle(fontSize: 15, color: Color(0xFFD4D4D4)),
208+
),
209+
const SizedBox(height: 8),
210+
Text(
211+
subtitle,
212+
textAlign: TextAlign.center,
213+
style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)),
214+
),
215+
],
216+
),
135217
),
136218
);
137219
}

0 commit comments

Comments
 (0)