1+ import 'dart:async' ;
2+
13import 'package:flutter/material.dart' ;
24import 'package:flutter_riverpod/flutter_riverpod.dart' ;
35import 'package:go_router/go_router.dart' ;
46
7+ import '../../../chat/domain/providers/session_provider.dart' ;
58import '../../domain/providers/git_provider.dart' ;
69import '../widgets/file_change_tile.dart' ;
710import '../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.
1416class GitScreen extends ConsumerStatefulWidget {
1517 final String sessionId;
1618
@@ -21,117 +23,197 @@ class GitScreen extends ConsumerStatefulWidget {
2123}
2224
2325class _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