Feature/ai chat l3 - #82
Conversation
…nhance AiChatService for dynamic base URL, and improve AiIntentResponse structure. Refactor AiWidgetFactory for better widget mapping and add new routes for deep linking. Update tests to reflect changes in widget behavior.
…ter. Update AiChatScreen and AiChatNotifier to streamline chat functionality. Adjust documentation to reflect changes in API usage.
📝 WalkthroughWalkthroughThe change updates AI chat routing, bearer-based identity, artifact rendering, and navigation. It adds API-key management and token refresh handling, updates local AI service configuration, and adds smoke-test and API-key handoff documentation. ChangesAI chat integration
API-key access
Documentation correction
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AiChatScreen
participant AiChatNotifier
participant AiChatService
participant FinanceAgent
AiChatScreen->>AiChatNotifier: sendMessage(text)
AiChatNotifier->>AiChatService: chat(message, sessionId)
AiChatService->>FinanceAgent: POST /api/v1/ai/chat
FinanceAgent-->>AiChatService: artifactType and data
AiChatService-->>AiChatNotifier: parsed AiIntentResponse
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (1)
am_app/lib/core/router/deferred_routes.dart (1)
232-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused
userIdparameter frombuildAiChatRoute.
AiChatScreenis now constructed without any arguments, souserId: _userId(context)is ignored. Droprequired String userIdfrombuildAiChatRouteand update theam_app/lib/core/router/app_router.dartcall site.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@am_app/lib/core/router/deferred_routes.dart` at line 232, Update buildAiChatRoute to remove the unused required String userId parameter, then update its call site in app_router.dart to stop passing userId while preserving the no-argument AiChatScreen construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@am_ai_ui/lib/data/ai_intent_response.dart`:
- Around line 12-21: Update both direct AiIntentResponse constructor calls in
portfolio_summary_card_test.dart to provide the required artifactType argument,
using the appropriate artifact type for each test case while leaving the
existing message, data, and other arguments unchanged.
- Around line 43-49: Update AiIntentResponse.fromJson so an unrecognized
non-empty artifactType is preserved as the widget identifier when no legacy
widgetId exists, instead of falling back to TEXT_RESPONSE. Keep mapped known
artifact types, legacyWidget precedence, and the existing TEXT_RESPONSE default
for missing or empty values unchanged.
- Around line 51-58: Update the parameter normalization logic in the response
parsing method so top-level data is always stored as params['data'] when
widgetParams does not already contain that key. Remove the branch that copies
map fields into the parameter root, while preserving existing
widgetParams['data'] values and handling non-null scalar or map data
consistently.
In `@am_ai_ui/lib/presentation/widgets/ai_data_intent_card.dart`:
- Around line 70-72: Update the data extraction in AiDataIntentCard.build so
widgetParams['data'] is only treated as a Map<String, dynamic> when its runtime
type matches; otherwise use the existing preview flow’s safe empty or fallback
value before calling _previewLines. Preserve map-based artifact previews while
preventing list or scalar data from throwing during build.
In `@am_auth_ui/lib/core/network/auth_interceptor.dart`:
- Around line 30-50: The concurrent 401 errors each trigger independent token
refresh requests with the same refresh token, causing failures if the identity
server invalidates the token after the first refresh. Serialize the refresh
attempts by introducing a class instance variable to store the in-flight refresh
Future. Before starting a new Dio().post refresh request in the 401 error
handler, check if a refresh Future already exists and await that instead. If no
Future exists yet, create and store the refresh operation before proceeding with
the current try-catch logic. After the refresh completes, clear the stored
Future so subsequent 401 batches can initiate a fresh refresh. This ensures all
concurrent requests await the same token refresh result rather than each
starting independent refreshes.
- Around line 35-38: Validate AuthEndpoints.identityRefreshToken before the
refresh POST, rejecting remote HTTP URLs while permitting HTTP only for loopback
development hosts; use the trusted configured URL source and preserve HTTPS
endpoints. Ensure the refresh request is not sent when validation fails.
- Around line 35-38: The refresh token request at
AuthEndpoints.identityRefreshToken uses a bare Dio() instance which lacks
timeout configuration, causing it to ignore the 30-second timeout policy.
Replace the new Dio() instance with a configured client that has explicit
connectTimeout, sendTimeout, and receiveTimeout values set, either by reusing
AuthProviders.dio or creating a dedicated recovery client with matching timeout
configuration. Preserve the endpoint URL and refresh token data payload
unchanged.
In `@am_common/lib/core/config/env_domains.dart`:
- Around line 75-77: Update the financeAgent endpoint handling in EnvDomains and
the aiChatServiceProvider setup so Authorization credentials are sent only to
HTTPS URLs; reject or fail provider initialization when the resolved endpoint,
including the http://localhost:8101 fallback, is non-HTTPS before
AuthInterceptor attaches the token.
In `@am_user_ui/lib/features/profile/presentation/pages/api_keys_page.dart`:
- Around line 98-104: The JSON snippet being generated in the
JsonEncoder.withIndent call includes a placeholder path `/path/to/asrax_mcp.py`
under the 'command' args that users will copy as-is, causing their settings to
fail. Replace this placeholder with either an actual supported launcher path
that works out of the box, or if a placeholder must be used, add clear
user-facing instructions (such as a note in the dialog or tooltip) explicitly
stating that the user must replace this path value with their actual
installation path before using the settings.
- Around line 108-147: The `barrierDismissible: false` parameter prevents
dismissal by tapping outside the dialog but does not block the system back
button. Wrap the AlertDialog in a WillPopScope or PopScope widget (depending on
your Flutter version) that prevents back navigation by returning false from the
onWillPop callback. Keep the 'I saved it' button with its
Navigator.pop(dialogContext) call as the only allowed way for users to close the
dialog.
In `@docs/AI_CHAT_SMOKE.md`:
- Around line 39-44: Fix the orphaned “5.” under the “## 3. UI” section by
renumbering it consistently with preceding smoke-test steps or converting it to
an appropriate bullet/plain sentence, while preserving its instruction about
portfolio cards and deep links.
In `@docs/HANDOFF_API_KEYS.md`:
- Line 57: Update the documented launcher path in the statement about building
the Cursor MCP launcher snippet. Replace the reference to `scripts/asrax_mcp.py`
with the actual path that the implementation emits, which is
`/path/to/asrax_mcp.py`, to ensure the documentation accurately reflects the
implementation behavior.
- Line 8: Remove the workstation-specific elements from the handoff
documentation that reference transient uncommitted state and absolute file
paths. Replace these with portable, repository-relative git commands that any
contributor can execute, such as git status --short --branch. Apply this change
to line 8 and also to the related instances on lines 115-116. Reference PR 82 to
guide the replacement approach.
---
Nitpick comments:
In `@am_app/lib/core/router/deferred_routes.dart`:
- Line 232: Update buildAiChatRoute to remove the unused required String userId
parameter, then update its call site in app_router.dart to stop passing userId
while preserving the no-argument AiChatScreen construction.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e81bd3d3-c4e2-491f-b03f-abe413f84ef2
📒 Files selected for processing (21)
README.mdam_ai_ui/lib/data/ai_chat_service.dartam_ai_ui/lib/data/ai_intent_response.dartam_ai_ui/lib/presentation/providers/ai_chat_provider.dartam_ai_ui/lib/presentation/screens/ai_chat_screen.dartam_ai_ui/lib/presentation/widgets/ai_data_intent_card.dartam_ai_ui/lib/presentation/widgets/ai_widget_factory.dartam_ai_ui/live/lib/main.dartam_ai_ui/pubspec.yamlam_ai_ui/test/widget/ai_widget_factory_test.dartam_app/lib/core/router/deferred_routes.dartam_app/web/config.local.example.jsonam_auth_ui/lib/am_auth_ui.dartam_auth_ui/lib/core/network/auth_interceptor.dartam_auth_ui/lib/di/auth_providers.dartam_common/lib/core/config/env_domains.dartam_doc_intelligence_ui/lib/services/api_service.dartam_user_ui/lib/features/profile/presentation/pages/api_keys_page.dartam_user_ui/lib/features/profile/presentation/pages/profile_settings_page.dartdocs/AI_CHAT_SMOKE.mddocs/HANDOFF_API_KEYS.md
| const AiIntentResponse({ | ||
| required this.message, | ||
| required this.artifactType, | ||
| this.data, | ||
| required this.widgetId, | ||
| required this.widgetParams, | ||
| required this.sessionId, | ||
| required this.toolsUsed, | ||
| required this.traceId, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 12 --glob '*.dart' '\bAiIntentResponse\s*\(' am_ai_uiRepository: AM-Portfolio/am-modern-ui
Length of output: 17072
Add artifactType to the direct AiIntentResponse constructors.
artifactType is required, but both AiIntentResponse calls in am_ai_ui/test/widget/portfolio_summary_card_test.dart omit it. Add the artifact type at each call site so the tests compile.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_ai_ui/lib/data/ai_intent_response.dart` around lines 12 - 21, Update both
direct AiIntentResponse constructor calls in portfolio_summary_card_test.dart to
provide the required artifactType argument, using the appropriate artifact type
for each test case while leaving the existing message, data, and other arguments
unchanged.
| factory AiIntentResponse.fromJson(Map<String, dynamic> json) { | ||
| final artifact = (json['artifactType'] as String?)?.trim(); | ||
| final legacyWidget = json['widgetId'] as String?; | ||
| final mapped = artifact != null && artifact.isNotEmpty | ||
| ? (_artifactToWidget[artifact] ?? 'TEXT_RESPONSE') | ||
| : null; | ||
| final widgetId = mapped ?? legacyWidget ?? 'TEXT_RESPONSE'; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep unknown artifact types unregistered.
Line 47 converts every unsupported artifactType into TEXT_RESPONSE. AiWidgetFactory.build resolves that registered builder first and returns SizedBox.shrink. The generic data-card branch at am_ai_ui/lib/presentation/widgets/ai_widget_factory.dart Lines 88-99 cannot run. Preserve an unknown artifact type when no legacy widget ID exists.
Proposed fix
- final mapped = artifact != null && artifact.isNotEmpty
- ? (_artifactToWidget[artifact] ?? 'TEXT_RESPONSE')
- : null;
- final widgetId = mapped ?? legacyWidget ?? 'TEXT_RESPONSE';
+ final hasArtifact = artifact != null && artifact.isNotEmpty;
+ final mapped = hasArtifact ? _artifactToWidget[artifact!] : null;
+ final widgetId =
+ mapped ?? legacyWidget ?? (hasArtifact ? artifact! : 'TEXT_RESPONSE');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| factory AiIntentResponse.fromJson(Map<String, dynamic> json) { | |
| final artifact = (json['artifactType'] as String?)?.trim(); | |
| final legacyWidget = json['widgetId'] as String?; | |
| final mapped = artifact != null && artifact.isNotEmpty | |
| ? (_artifactToWidget[artifact] ?? 'TEXT_RESPONSE') | |
| : null; | |
| final widgetId = mapped ?? legacyWidget ?? 'TEXT_RESPONSE'; | |
| factory AiIntentResponse.fromJson(Map<String, dynamic> json) { | |
| final artifact = (json['artifactType'] as String?)?.trim(); | |
| final legacyWidget = json['widgetId'] as String?; | |
| final hasArtifact = artifact != null && artifact.isNotEmpty; | |
| final mapped = hasArtifact ? _artifactToWidget[artifact!] : null; | |
| final widgetId = | |
| mapped ?? legacyWidget ?? (hasArtifact ? artifact! : 'TEXT_RESPONSE'); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_ai_ui/lib/data/ai_intent_response.dart` around lines 43 - 49, Update
AiIntentResponse.fromJson so an unrecognized non-empty artifactType is preserved
as the widget identifier when no legacy widgetId exists, instead of falling back
to TEXT_RESPONSE. Keep mapped known artifact types, legacyWidget precedence, and
the existing TEXT_RESPONSE default for missing or empty values unchanged.
| Map<String, dynamic> params = | ||
| (json['widgetParams'] as Map<String, dynamic>?) ?? {}; | ||
| final data = json['data']; | ||
| if (params.isEmpty && data is Map<String, dynamic>) { | ||
| params = Map<String, dynamic>.from(data); | ||
| } else if (params.isEmpty && data != null) { | ||
| params = {'data': data}; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Keep top-level data under widgetParams['data'].
When a response has top-level data and no widgetParams, Line 55 copies map fields to the parameter root. AiDataIntentCard and _PortfolioSummaryCard read widgetParams['data'], so valid artifact data renders as missing. If widgetParams is nonempty without data, the top-level value is ignored. Set params['data'] unless the response already provides that key.
Proposed fix
- Map<String, dynamic> params =
- (json['widgetParams'] as Map<String, dynamic>?) ?? {};
+ final params = Map<String, dynamic>.from(
+ (json['widgetParams'] as Map<String, dynamic>?) ?? {},
+ );
final data = json['data'];
- if (params.isEmpty && data is Map<String, dynamic>) {
- params = Map<String, dynamic>.from(data);
- } else if (params.isEmpty && data != null) {
+ if (data != null && !params.containsKey('data')) {
params = {'data': data};
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_ai_ui/lib/data/ai_intent_response.dart` around lines 51 - 58, Update the
parameter normalization logic in the response parsing method so top-level data
is always stored as params['data'] when widgetParams does not already contain
that key. Remove the branch that copies map fields into the parameter root,
while preserving existing widgetParams['data'] values and handling non-null
scalar or map data consistently.
| Widget build(BuildContext context) { | ||
| final data = widgetParams['data'] as Map<String, dynamic>?; | ||
| final lines = _previewLines(data); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard the artifact-data cast.
AiIntentResponse.data can be a list or scalar. The parser preserves those values in widgetParams['data']. Line 71 throws during widget build when such a value is cast to Map<String, dynamic>. Check the runtime type before generating preview lines.
Proposed fix
- final data = widgetParams['data'] as Map<String, dynamic>?;
+ final rawData = widgetParams['data'];
+ final data = rawData is Map<String, dynamic> ? rawData : null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Widget build(BuildContext context) { | |
| final data = widgetParams['data'] as Map<String, dynamic>?; | |
| final lines = _previewLines(data); | |
| Widget build(BuildContext context) { | |
| final rawData = widgetParams['data']; | |
| final data = rawData is Map<String, dynamic> ? rawData : null; | |
| final lines = _previewLines(data); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_ai_ui/lib/presentation/widgets/ai_data_intent_card.dart` around lines 70 -
72, Update the data extraction in AiDataIntentCard.build so widgetParams['data']
is only treated as a Map<String, dynamic> when its runtime type matches;
otherwise use the existing preview flow’s safe empty or fallback value before
calling _previewLines. Preserve map-based artifact previews while preventing
list or scalar data from throwing during build.
| if (err.response?.statusCode == 401 && | ||
| err.requestOptions.extra['authRefreshRetried'] != true) { | ||
| final refreshToken = await _storageService.getRefreshToken(); | ||
| if (refreshToken != null && refreshToken.isNotEmpty) { | ||
| try { | ||
| final refreshResponse = await Dio().post<Map<String, dynamic>>( | ||
| AuthEndpoints.identityRefreshToken, | ||
| data: {'refresh_token': refreshToken}, | ||
| ); | ||
| final data = refreshResponse.data!; | ||
| final accessToken = data['access_token'] as String; | ||
| await _storageService.saveAccessToken(accessToken); | ||
| final nextRefreshToken = data['refresh_token'] as String?; | ||
| if (nextRefreshToken != null && nextRefreshToken.isNotEmpty) { | ||
| await _storageService.saveRefreshToken(nextRefreshToken); | ||
| } | ||
|
|
||
| final options = err.requestOptions; | ||
| options.extra['authRefreshRetried'] = true; | ||
| options.headers['Authorization'] = 'Bearer $accessToken'; | ||
| final response = await Dio().fetch<dynamic>(options); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Serialize concurrent token refreshes.
Each concurrent 401 starts a separate refresh with the same stored token. If identity rotates and invalidates that token, only the first refresh succeeds. The remaining requests return their original 401 errors.
Store one shared refresh Future. Let concurrent requests await it before replaying with the resulting access token.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_auth_ui/lib/core/network/auth_interceptor.dart` around lines 30 - 50, The
concurrent 401 errors each trigger independent token refresh requests with the
same refresh token, causing failures if the identity server invalidates the
token after the first refresh. Serialize the refresh attempts by introducing a
class instance variable to store the in-flight refresh Future. Before starting a
new Dio().post refresh request in the 401 error handler, check if a refresh
Future already exists and await that instead. If no Future exists yet, create
and store the refresh operation before proceeding with the current try-catch
logic. After the refresh completes, clear the stored Future so subsequent 401
batches can initiate a fresh refresh. This ensures all concurrent requests await
the same token refresh result rather than each starting independent refreshes.
| final snippet = const JsonEncoder.withIndent(' ').convert({ | ||
| 'mcpServers': { | ||
| 'asrax-finance': { | ||
| 'command': 'python', | ||
| 'args': ['/path/to/asrax_mcp.py'], | ||
| 'env': {'ASRAX_KEY_ID': keyId, 'ASRAX_KEY_SECRET': secret}, | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not copy an unexplained placeholder path.
/path/to/asrax_mcp.py makes the copied settings fail until the user edits them. The dialog presents the value as ready-to-use Cursor settings.
Generate the supported launcher path, or state clearly that the user must replace this value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_user_ui/lib/features/profile/presentation/pages/api_keys_page.dart` around
lines 98 - 104, The JSON snippet being generated in the JsonEncoder.withIndent
call includes a placeholder path `/path/to/asrax_mcp.py` under the 'command'
args that users will copy as-is, causing their settings to fail. Replace this
placeholder with either an actual supported launcher path that works out of the
box, or if a placeholder must be used, add clear user-facing instructions (such
as a note in the dialog or tooltip) explicitly stating that the user must
replace this path value with their actual installation path before using the
settings.
| await showDialog<void>( | ||
| context: context, | ||
| barrierDismissible: false, | ||
| builder: (dialogContext) => AlertDialog( | ||
| title: const Text('Save this secret now'), | ||
| content: SizedBox( | ||
| width: 620, | ||
| child: SingleChildScrollView( | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| const Text( | ||
| 'The secret is shown once and cannot be retrieved later.', | ||
| ), | ||
| const SizedBox(height: 16), | ||
| SelectableText('Key ID: $keyId\nSecret: $secret'), | ||
| const SizedBox(height: 16), | ||
| const Text('Cursor MCP settings:'), | ||
| const SizedBox(height: 8), | ||
| SelectableText(snippet), | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| actions: [ | ||
| TextButton.icon( | ||
| onPressed: () async { | ||
| await Clipboard.setData(ClipboardData(text: snippet)); | ||
| }, | ||
| icon: const Icon(Icons.copy_rounded), | ||
| label: const Text('Copy settings'), | ||
| ), | ||
| FilledButton( | ||
| onPressed: () => Navigator.pop(dialogContext), | ||
| child: const Text('I saved it'), | ||
| ), | ||
| ], | ||
| ), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the Flutter constraint and locate existing dialog pop-guard patterns.
fd -a 'pubspec.yaml|pubspec.lock' . -x rg -n -C3 'flutter:|sdk:'
rg -n -C4 'PopScope|WillPopScope|barrierDismissible:\s*false' \
--glob '*.dart' .Repository: AM-Portfolio/am-modern-ui
Length of output: 20625
🌐 Web query:
Flutter MaterialApp router PopScope canPop false system back button does not pop route
💡 Result:
When you set canPop to false on a PopScope widget, it correctly blocks the system back gesture from popping the current route [1][2][3]. This is the expected and intended behavior of the API [4]. When canPop is false: - The system back gesture will not pop the route [1][3]. - The onPopInvokedWithResult callback will still be triggered if a back gesture is attempted [1][3][5]. - Within this callback, the didPop parameter will be false, indicating that the pop was blocked [1][4][3]. If you need to perform an action (like showing a confirmation dialog) before allowing the user to exit the page, you should use this pattern [4][6]: 1. Set canPop to false (or a variable that resolves to false when you want to block the pop) [4][6]. 2. Use onPopInvokedWithResult to listen for the attempted pop [4][6]. 3. Inside onPopInvokedWithResult, check if didPop is false. If it is, run your custom logic (e.g., showing a dialog) [4][6]. 4. If your custom logic confirms the user wants to leave, call Navigator.of(context).pop to manually pop the route [4][6]. Important notes: - Predictive back animations on Android are disabled when canPop is false [2][3]. - On iOS, when using CupertinoRouteTransitionMixin, setting canPop to false may result in the back gesture not being detected at all, meaning onPopInvokedWithResult might not be called [1][3]. - If multiple PopScope widgets are present in the widget tree, every single one must have canPop set to true for the route to be popped [2][3].
Citations:
- 1: https://api.flutter.dev/flutter/widgets/PopScope-class.html
- 2: https://api.flutter.dev/flutter/widgets/PopScope/canPop.html
- 3: https://github.com/flutter/flutter/blob/main/packages/flutter/lib/src/widgets/pop_scope.dart
- 4: https://docs.flutter.dev/release/breaking-changes/android-predictive-back
- 5: https://api.flutter.dev/flutter/widgets/PopScope/onPopInvokedWithResult.html
- 6: https://stackoverflow.com/questions/45916658/how-to-deactivate-or-override-the-android-back-button-in-flutter
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a 'api_keys_page.dart' . -x sh -c '
echo "--- $0 ---"
wc -l "$0"
sed -n "1,210p" "$0"
'Repository: AM-Portfolio/am-modern-ui
Length of output: 6732
Block system-back dismissal of the one-time secret.
barrierDismissible: false only prevents tapping outside the dialog. It does not stop the system back button from closing it. Use a system-back guard around the confirmation step, then allow the return only after the user selects I saved it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@am_user_ui/lib/features/profile/presentation/pages/api_keys_page.dart` around
lines 108 - 147, The `barrierDismissible: false` parameter prevents dismissal by
tapping outside the dialog but does not block the system back button. Wrap the
AlertDialog in a WillPopScope or PopScope widget (depending on your Flutter
version) that prevents back navigation by returning false from the onWillPop
callback. Keep the 'I saved it' button with its Navigator.pop(dialogContext)
call as the only allowed way for users to close the dialog.
| ## 3. UI | ||
|
|
||
| Deployed UI uses `domain: am-dev.asrax.in` → `EnvDomains` resolves chat to `$apiBase/ai`. | ||
| Local override: set `aiGateway` to `https://am-dev.asrax.in/ai` in `config.local.json`. | ||
| 5. Ask for portfolio summary / holdings / top movers → expect cards; **View details** deep-links into portfolio / trade / analysis routes. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the orphaned list numbering under "## 3. UI".
Line 43 starts with 5. but no items 1-4 precede it in this section. Renumber or convert this line to a plain sentence or bullet, so the list is not misleading to a reader following the smoke-test steps.
📝 Proposed fix
Local override: set `aiGateway` to `https://am-dev.asrax.in/ai` in `config.local.json`.
-5. Ask for portfolio summary / holdings / top movers → expect cards; **View details** deep-links into portfolio / trade / analysis routes.
+Ask for portfolio summary / holdings / top movers → expect cards; **View details** deep-links into portfolio / trade / analysis routes.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## 3. UI | |
| Deployed UI uses `domain: am-dev.asrax.in` → `EnvDomains` resolves chat to `$apiBase/ai`. | |
| Local override: set `aiGateway` to `https://am-dev.asrax.in/ai` in `config.local.json`. | |
| 5. Ask for portfolio summary / holdings / top movers → expect cards; **View details** deep-links into portfolio / trade / analysis routes. | |
| ## 3. UI | |
| Deployed UI uses `domain: am-dev.asrax.in` → `EnvDomains` resolves chat to `$apiBase/ai`. | |
| Local override: set `aiGateway` to `https://am-dev.asrax.in/ai` in `config.local.json`. | |
| Ask for portfolio summary / holdings / top movers → expect cards; **View details** deep-links into portfolio / trade / analysis routes. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/AI_CHAT_SMOKE.md` around lines 39 - 44, Fix the orphaned “5.” under the
“## 3. UI” section by renumbering it consistently with preceding smoke-test
steps or converting it to an appropriate bullet/plain sentence, while preserving
its instruction about portfolio cards and deep links.
| - The API Keys settings page and one-time secret flow exist in the local tree. | ||
| - Profile Settings already imports and opens `ApiKeysPage`; basic navigation is linked. | ||
| - The auth interceptor attaches Bearer auth and refreshes/replays once on HTTP 401. | ||
| - Repository: `am-modern-ui` on `feature/ai-chat-l3`, with uncommitted WIP. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove workstation-specific WIP instructions.
The handoff records transient uncommitted state and an absolute a:\InfraCode\... path. Other contributors cannot use this command.
Reference PR 82 and use repository-relative commands such as git status --short --branch.
Also applies to: 115-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/HANDOFF_API_KEYS.md` at line 8, Remove the workstation-specific elements
from the handoff documentation that reference transient uncommitted state and
absolute file paths. Replace these with portable, repository-relative git
commands that any contributor can execute, such as git status --short --branch.
Apply this change to line 8 and also to the related instances on lines 115-116.
Reference PR 82 to guide the replacement approach.
| - The create dialog clearly says the secret is shown once. | ||
| - The dialog cannot be dismissed by tapping outside. | ||
| - It displays the key ID and secret after creation. | ||
| - It builds a Cursor MCP launcher snippet using `scripts/asrax_mcp.py`. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Match the documented launcher path to the implementation.
The implementation emits /path/to/asrax_mcp.py, not scripts/asrax_mcp.py. Update this statement after selecting the supported launcher-path behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/HANDOFF_API_KEYS.md` at line 57, Update the documented launcher path in
the statement about building the Cursor MCP launcher snippet. Replace the
reference to `scripts/asrax_mcp.py` with the actual path that the implementation
emits, which is `/path/to/asrax_mcp.py`, to ensure the documentation accurately
reflects the implementation behavior.
Summary by CodeRabbit