Skip to content

Feature/ai chat l3 - #82

Open
ssd2658 wants to merge 2 commits into
mainfrom
feature/ai-chat-l3
Open

Feature/ai chat l3#82
ssd2658 wants to merge 2 commits into
mainfrom
feature/ai-chat-l3

Conversation

@ssd2658

@ssd2658 ssd2658 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added richer AI chat responses with data previews, portfolio summaries, and navigation to detailed views.
    • Added an API Keys page for creating, viewing, copying, and revoking access keys.
    • Added Account settings access to API key management.
  • Bug Fixes
    • Improved session handling by automatically refreshing expired authentication tokens.
    • Improved AI chat connectivity and configuration across environments.
  • Documentation
    • Added AI chat smoke-test guidance and API key integration handoff documentation.

sahim99 added 2 commits August 3, 2026 14:44
…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.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

AI chat integration

Layer / File(s) Summary
AI chat routing and response contract
am_common/lib/core/config/env_domains.dart, am_ai_ui/lib/data/*
The chat service uses EnvDomains.financeAgent and /api/v1/ai/chat. AiIntentResponse now carries artifact types and optional data.
Artifact widget rendering
am_ai_ui/lib/presentation/widgets/*, am_ai_ui/test/widget/*
The widget factory uses registered builders and AiDataIntentCard. Portfolio and fallback cards support detail navigation. Tests use artifact types.
AI chat application wiring
am_ai_ui/lib/presentation/providers/*, am_ai_ui/lib/presentation/screens/*, am_ai_ui/live/*, am_app/lib/core/router/*, am_app/web/*, README.md, docs/AI_CHAT_SMOKE.md
AI chat no longer passes userId. The provider uses a 60-second timeout, and local configuration and smoke-test documentation describe AI routing.

API-key access

Layer / File(s) Summary
Authentication refresh handling
am_auth_ui/lib/*
The interceptor refreshes expired tokens once, stores refreshed credentials, retries the request, and registers only once.
API-key management and navigation
am_user_ui/lib/features/profile/presentation/pages/*, docs/HANDOFF_API_KEYS.md
ApiKeysPage supports API-key listing, creation, secret display, copying, and revocation. Profile settings adds navigation to the page. The handoff document records the flow.

Documentation correction

Layer / File(s) Summary
Document-processing routing note
am_doc_intelligence_ui/lib/services/api_service.dart
The routing comment now names api-gateway (am-api-gateway).

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
Loading

Possibly related PRs

Suggested reviewers: asraxsoumya

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change as the AI chat L3 feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ai-chat-l3

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (1)
am_app/lib/core/router/deferred_routes.dart (1)

232-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused userId parameter from buildAiChatRoute.

AiChatScreen is now constructed without any arguments, so userId: _userId(context) is ignored. Drop required String userId from buildAiChatRoute and update the am_app/lib/core/router/app_router.dart call 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

📥 Commits

Reviewing files that changed from the base of the PR and between 141a728 and 7397f4f.

📒 Files selected for processing (21)
  • README.md
  • am_ai_ui/lib/data/ai_chat_service.dart
  • am_ai_ui/lib/data/ai_intent_response.dart
  • am_ai_ui/lib/presentation/providers/ai_chat_provider.dart
  • am_ai_ui/lib/presentation/screens/ai_chat_screen.dart
  • am_ai_ui/lib/presentation/widgets/ai_data_intent_card.dart
  • am_ai_ui/lib/presentation/widgets/ai_widget_factory.dart
  • am_ai_ui/live/lib/main.dart
  • am_ai_ui/pubspec.yaml
  • am_ai_ui/test/widget/ai_widget_factory_test.dart
  • am_app/lib/core/router/deferred_routes.dart
  • am_app/web/config.local.example.json
  • am_auth_ui/lib/am_auth_ui.dart
  • am_auth_ui/lib/core/network/auth_interceptor.dart
  • am_auth_ui/lib/di/auth_providers.dart
  • am_common/lib/core/config/env_domains.dart
  • am_doc_intelligence_ui/lib/services/api_service.dart
  • am_user_ui/lib/features/profile/presentation/pages/api_keys_page.dart
  • am_user_ui/lib/features/profile/presentation/pages/profile_settings_page.dart
  • docs/AI_CHAT_SMOKE.md
  • docs/HANDOFF_API_KEYS.md

Comment on lines 12 to 21
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,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 --glob '*.dart' '\bAiIntentResponse\s*\(' am_ai_ui

Repository: 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.

Comment on lines 43 to +49
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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +51 to +58
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};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +70 to +72
Widget build(BuildContext context) {
final data = widgetParams['data'] as Map<String, dynamic>?;
final lines = _previewLines(data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +30 to +50
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +98 to +104
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},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +108 to +147
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'),
),
],
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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:


🏁 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.

Comment thread docs/AI_CHAT_SMOKE.md
Comment on lines +39 to +44
## 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
## 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.

Comment thread docs/HANDOFF_API_KEYS.md
- 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment thread docs/HANDOFF_API_KEYS.md
- 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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants