Skip to content

Feat/log detail 3 mode toggle - #16

Merged
buivietphi merged 9 commits into
mainfrom
feat/log-detail-3-mode-toggle
Jul 11, 2026
Merged

buivietphi merged 9 commits into
mainfrom
feat/log-detail-3-mode-toggle

Conversation

@buivietphi

@buivietphi buivietphi commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Related Issue

Type of Change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation update
  • Performance improvement
  • CI/CD or build configuration
  • Other (describe below)

Testing

  • Tested on macOS
  • Tested on Windows
  • Flutter analyze passes
  • SDK build passes (if applicable)

Screenshots (if applicable)

Summary by CodeRabbit

  • New Features
    • Added configurable data-retention limits plus separate “All Events” display cap, with “showing N of M” hints and untrimmed counts across multiple screens.
    • Added query-parameter tabs and improved URL display (compact/pretty/one-line) with Fetch/XHR “via” badges and copy actions.
  • Bug Fixes
    • Improved network request instrumentation to avoid duplicate reporting and better handle non-text responses.
  • Improvements
    • Persisted theme mode and tab visibility selections.
  • Localization
    • Added translations for data-retention and “All Events” display settings.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d70a0cf6-766f-4600-b18f-9656f1958ce0

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc9d16 and dd5d1cf.

⛔ Files ignored due to path filters (1)
  • .github/workflows/pr-verification.yml is excluded by none and included by none
📒 Files selected for processing (10)
  • lib/core/providers/retention_provider.dart
  • lib/core/utils/network_url_formatter.dart
  • lib/features/all_events/presentation/detail/network_detail.dart
  • lib/features/benchmark/provider/benchmark_providers.dart
  • lib/features/console/provider/console_providers.dart
  • lib/features/display/provider/display_providers.dart
  • lib/features/error_inspector/provider/error_providers.dart
  • lib/features/network_inspector/provider/network_providers.dart
  • lib/features/performance/provider/performance_providers.dart
  • lib/features/state_inspector/provider/state_providers.dart
🚧 Files skipped from review as they are similar to previous changes (10)
  • lib/features/error_inspector/provider/error_providers.dart
  • lib/features/state_inspector/provider/state_providers.dart
  • lib/features/benchmark/provider/benchmark_providers.dart
  • lib/features/all_events/presentation/detail/network_detail.dart
  • lib/features/performance/provider/performance_providers.dart
  • lib/features/display/provider/display_providers.dart
  • lib/features/console/provider/console_providers.dart
  • lib/core/providers/retention_provider.dart
  • lib/features/network_inspector/provider/network_providers.dart
  • lib/core/utils/network_url_formatter.dart

📝 Walkthrough

Walkthrough

This PR adds persisted retention controls and capped display providers across event streams, improves network interception and URL presentation, adds query-parameter tabs and interceptor badges, updates detail rendering, and introduces localized settings text.

Changes

Retention and persisted settings

Layer / File(s) Summary
Retention contracts and persisted preferences
lib/core/providers/*, lib/core/utils/*, lib/core/theme/*
Retention presets, capped-list utilities, theme persistence, and tab visibility persistence are added.
Retention-capped event providers
lib/features/*/provider/*
Feature providers apply retention caps, filter capped items, replace manual trimming, and clean up subscriptions.
Retention settings and count indicators
lib/components/misc/retention_hint.dart, lib/features/settings/..., lib/features/*/presentation/...
Settings controls and count indicators show configured limits and pre-cap totals.
Retention localization resources
lib/l10n/*
Retention and All Events Display strings are added across supported locales.

Network observability and inspector presentation

Layer / File(s) Summary
React Native interceptor lifecycle and deduplication
client_sdks/devconnect-react-native/src/client.ts
Initialization, fetch/XHR deduplication, interceptor metadata, and non-text response handling are updated.
Network metadata and URL formatting
lib/models/network/*, lib/server/*, lib/core/utils/network_url_formatter.dart
Network entries preserve interceptor origin, and shared URL parsing and formatting helpers are introduced.
Network cards, details, and parameter tabs
lib/features/network_inspector/..., lib/features/all_events/presentation/...
Network URLs, query parameters, via badges, dynamic tabs, copy actions, and responsive layouts are updated.
All Events detail rendering
lib/features/all_events/presentation/detail/*, event_row/*
Shared text/JSON components and formatted network screenshot content are used in detail views.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

Fetch and XHR now share the stage,
Retention trims the scrolling page.
URLs unfold, params shine,
Counts reveal the hidden line.
Settings bloom in every tongue.

🚥 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 is concise and matches the main feature change: a three-mode log detail toggle.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/log-detail-3-mode-toggle

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a data retention feature with hard caps and view-only display limits across various event and log lists, along with UI enhancements like a dedicated "Params" tab for network requests and support for tracking the interceptor path (fetch vs. XHR). The review feedback highlights a critical issue where passing null to truncateList disables list truncation entirely, causing memory leaks and breaking the data retention hard cap across multiple providers. Additionally, potential FormatException crashes in the URL parser due to invalid percent-encoding should be resolved by extending the try-catch block, and dynamic recreation of the TabController during the build phase should be avoided to prevent state inconsistency.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +58 to 62
ConsoleNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onLog.listen((entry) {
if (state.length > 10000) {
state = [...state.skip(1000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory log list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 10000) if the limit is null (Unlimited).

Suggested change
ConsoleNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onLog.listen((entry) {
if (state.length > 10000) {
state = [...state.skip(1000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
ConsoleNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onLog.listen((entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 10000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines 150 to 155
updated[index] = _mergeNetworkEntries(state[index], entry);
state = updated;
} else {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory network entry list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

      } else {
        final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
        state = truncateList([...state, entry], limit);
      }

Comment on lines +106 to 110
ErrorNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onError.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory error list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

Suggested change
ErrorNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onError.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
ErrorNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onError.listen((entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines +61 to 65
StateChangesNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onState.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory state changes list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

Suggested change
StateChangesNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onState.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
StateChangesNotifier(WsMessageHandler wsMessageHandler, this._ref) : super([]) {
_sub = wsMessageHandler.onState.listen((entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines 40 to 42
void add(DisplayEntry entry) {
if (state.length >= 5000) {
state = [...state.sublist(state.length - 4000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory display entries list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

Suggested change
void add(DisplayEntry entry) {
if (state.length >= 5000) {
state = [...state.sublist(state.length - 4000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
}
void add(DisplayEntry entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
state = truncateList([...state, entry], limit);
}

Comment on lines 111 to 116
steps: steps,
);

if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory benchmark list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

Suggested change
steps: steps,
);
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
steps: steps,
);
final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines +353 to 357
PerformanceNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onPerformance.listen((entry) {
if (state.length > 10000) {
state = [...state.skip(1000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory performance list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 10000) if the limit is null (Unlimited).

Suggested change
PerformanceNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onPerformance.listen((entry) {
if (state.length > 10000) {
state = [...state.skip(1000), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
PerformanceNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onPerformance.listen((entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 10000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines +417 to 421
MemoryLeakNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onMemoryLeak.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

Passing null as the limit to truncateList disables list truncation entirely, causing the in-memory memory leak list to grow infinitely and leak memory. This also breaks the 'Data Retention' feature since the hard cap is never applied to the source list. You should read the user-configured limit from _ref and apply it, falling back to a safe maximum (e.g., 5000) if the limit is null (Unlimited).

Suggested change
MemoryLeakNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onMemoryLeak.listen((entry) {
if (state.length > 5000) {
state = [...state.skip(500), entry];
} else {
state = [...state, entry];
}
state = truncateList([...state, entry], null);
});
}
MemoryLeakNotifier(WsMessageHandler handler, this._ref) : super([]) {
_sub = handler.onMemoryLeak.listen((entry) {
final limit = _ref.read(retentionLimitProvider).limit ?? 5000;
state = truncateList([...state, entry], limit);
});
}

Comment on lines +46 to +74
FormattedUrl? parseFormattedUrl(String? url) {
if (url == null) return null;
final trimmed = url.trim();
if (trimmed.isEmpty || trimmed == '<unknown url>') return null;

Uri uri;
try {
uri = Uri.parse(trimmed);
} catch (_) {
return FormattedUrl(host: null, path: trimmed, queryParams: const [], raw: trimmed);
}

// `uri.queryParametersAll` keeps insertion order AND preserves repeated
// keys as lists — the former matters for `?order=` style params, the
// latter for Supabase-style `?id=in.(1,2,3)` filters.
final params = <FormattedQueryParam>[];
uri.queryParametersAll.forEach((k, values) {
for (final v in values) {
params.add(FormattedQueryParam(k, _decode(v)));
}
});

return FormattedUrl(
host: uri.host.isEmpty ? null : uri.host,
path: uri.path.isEmpty ? '/' : uri.path,
queryParams: params,
raw: trimmed,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Accessing uri.queryParametersAll can throw a FormatException if the query parameters contain invalid percent-encoding (e.g., ?q=%g1). Since this access is currently outside the try-catch block, it can crash the parser and any widget that calls it. Wrapping the entire parsing and query parameter extraction in the try-catch block will make it completely safe.

FormattedUrl? parseFormattedUrl(String? url) {
  if (url == null) return null;
  final trimmed = url.trim();
  if (trimmed.isEmpty || trimmed == '<unknown url>') return null;

  try {
    final uri = Uri.parse(trimmed);
    // `uri.queryParametersAll` keeps insertion order AND preserves repeated
    // keys as lists — the former matters for `?order=` style params, the
    // latter for Supabase-style `?id=in.(1,2,3)` filters.
    final params = <FormattedQueryParam>[];
    uri.queryParametersAll.forEach((k, values) {
      for (final v in values) {
        params.add(FormattedQueryParam(k, _decode(v)));
      }
    });

    return FormattedUrl(
      host: uri.host.isEmpty ? null : uri.host,
      path: uri.path.isEmpty ? '/' : uri.path,
      queryParams: params,
      raw: trimmed,
    );
  } catch (_) {
    return FormattedUrl(host: null, path: trimmed, queryParams: const [], raw: trimmed);
  }
}

Comment on lines +67 to +74
void _resizeControllerIfNeeded(int newLength) {
if (_tabController.length == newLength) return;
final oldIndex = _tabController.index.clamp(0, newLength - 1);
_tabController.removeListener(_onTabIndexChange);
_tabController.dispose();
_tabController = _makeController(oldIndex, newLength);
_tabController.addListener(_onTabIndexChange);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Disposing and recreating the TabController dynamically during the build phase is a bad practice in Flutter and can lead to state inconsistency, scroll controller errors, or 'setState() called during build' crashes. Since ParamsTab already handles the empty state gracefully with an EmptyState widget, it is safer to keep the tab count static or manage the tab controller recreation in didUpdateWidget instead of during build.

@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: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/features/all_events/presentation/pages/all_events_page.dart (1)

409-458: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore StableListView for the event list.

This replaces the required stable list implementation with ListView.custom, discarding the stable delegate behavior during live list updates. Use StableListView here instead. As per path instructions, “Use StableListView for list views.”

🤖 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 `@lib/features/all_events/presentation/pages/all_events_page.dart` around lines
409 - 458, Replace the ListView.custom in the event list with StableListView,
preserving the existing controller, item extent, item builder logic, keys, and
visible item count through StableListView’s corresponding API. Ensure the stable
delegate behavior remains active during live updates.

Source: Path instructions

lib/l10n/app_zh_TW.arb (1)

170-173: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Remove duplicate JSON keys.

Same issue as app_zh_CN.arb: noNetworkRequests and apiCallsAppearHere each appear twice (lines 170–173) with identical values. Remove the duplicate entries at lines 172–173.

🤖 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 `@lib/l10n/app_zh_TW.arb` around lines 170 - 173, Remove the duplicated
`noNetworkRequests` and `apiCallsAppearHere` entries from the localization
resource, keeping only one occurrence of each key and preserving their existing
values.
lib/l10n/app_zh_CN.arb (1)

170-173: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Remove duplicate JSON keys.

noNetworkRequests and apiCallsAppearHere each appear twice (lines 170–173) with identical values. Duplicate keys can cause issues with ARB code generation and are invalid JSON practice. Remove the duplicate entries at lines 172–173.

🤖 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 `@lib/l10n/app_zh_CN.arb` around lines 170 - 173, Remove the duplicate
noNetworkRequests and apiCallsAppearHere entries from the ARB resource,
retaining only one definition of each key and its existing value.
🧹 Nitpick comments (8)
lib/features/network_inspector/presentation/shared/params_tab.dart (1)

107-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Value cell uses SelectableText rather than TextComponent.

Same "Detail panels must use TextComponent for selectable text" path instruction as flagged in request_detail_panel.dart — this params tab is rendered inside that same detail panel and the value cell is deliberately selectable (see comment above it).

As per path instructions, "Detail panels must use TextComponent for selectable text."

🤖 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 `@lib/features/network_inspector/presentation/shared/params_tab.dart` around
lines 107 - 129, The selectable value in the params tab currently uses Flutter’s
SelectableText instead of the required TextComponent. In the value-cell widget
near widget.keyName and widget.value, replace SelectableText with TextComponent
while preserving the existing value, styling, and selectable-text behavior.

Source: Path instructions

lib/features/network_inspector/presentation/request/request_detail_panel.dart (4)

774-783: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same decode-with-fallback logic duplicated across files.

_decoded here duplicates the Uri.decodeFull/catch-fallback pattern also inlined in event_row.dart's _tooltipFor (and presumably network_detail.dart). Since lib/core/utils/network_url_formatter.dart already hosts URL-formatting helpers (e.g. formatUrlOneLine), a shared decodeUrlSafe(String) there would consolidate this and keep the fallback behavior consistent as more call sites are added (the "All Events detail rendering" layer needs the same thing).

🤖 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
`@lib/features/network_inspector/presentation/request/request_detail_panel.dart`
around lines 774 - 783, Add a shared decodeUrlSafe(String) helper to
network_url_formatter.dart that returns Uri.decodeFull(url) with the original
URL as fallback on decoding errors. Replace the local _decoded implementation
and duplicated inline decoding in event_row.dart's _tooltipFor and
network_detail.dart with this helper, updating imports and preserving existing
behavior.

219-270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "via" badge rendering vs. _ViaBadge in event_row.dart.

This inline Builder reimplements the same fetch/xhr → color/label mapping that _ViaBadge in lib/features/all_events/presentation/event_row/event_row.dart already encapsulates. Extracting a shared ViaBadge widget (parameterized by size/padding if needed) would avoid the two implementations drifting (e.g. this one derives the label via entry.via.toUpperCase() while the other hardcodes 'FETCH'/'XHR' per case).

🤖 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
`@lib/features/network_inspector/presentation/request/request_detail_panel.dart`
around lines 219 - 270, Replace the inline via badge Builder in the request
detail panel with a shared ViaBadge widget extracted from _ViaBadge in
event_row.dart. Centralize the NetworkVia fetch/xhr color and label mapping
there, supporting any required size or padding parameters, and update both call
sites to use it so labels and styling cannot diverge.

93-114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Disposing/recreating _tabController directly inside build() is fragile — consider didUpdateWidget.

Mutating _tabController (dispose + reassign) as a side effect of build() works today because the same pass immediately hands the new controller to DetailTabBar/TabBarView, but it's a latent trap: if this widget is ever driven by an ancestor rebuild unrelated to entry (e.g. a theme change) while TabController-consuming descendants haven't yet re-subscribed, this pattern risks "used after being disposed" errors down the line. The existing _rebuildController() (triggered from ref.listen, with setState) already gives you the right template — worth using an analogous check in didUpdateWidget(oldWidget) comparing oldWidget.entry.url vs widget.entry.url instead of doing it inline in build().

🤖 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
`@lib/features/network_inspector/presentation/request/request_detail_panel.dart`
around lines 93 - 114, Move the tab-controller length synchronization out of
build and into didUpdateWidget, comparing oldWidget.entry.url with
widget.entry.url before rebuilding. Reuse the existing _rebuildController
pattern to dispose and recreate _tabController, including clamped index
preservation and setState as appropriate; keep build focused on deriving tab
labels and rendering descendants without mutating controller state.

201-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use TextComponent for the selectable URL
TextComponent already supports selectable text (selectable defaults to true), so this detail panel should use it here instead of raw SelectableText to stay consistent with the panel pattern.

🤖 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
`@lib/features/network_inspector/presentation/request/request_detail_panel.dart`
around lines 201 - 217, The selectable URL currently uses raw SelectableText
instead of the panel’s standard TextComponent. Replace SelectableText in the URL
row with TextComponent, preserving the decoded URL, styling, padding, and
max-line behavior while relying on TextComponent’s default selectable support.

Source: Path instructions

client_sdks/devconnect-react-native/src/client.ts (1)

547-571: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

init() now resolves before the host is known / connection is attempted — update the docstring.

Previously await DevConnect.init() implied a connection attempt (with the real host) had already started by the time the promise resolved. Now the promise resolves immediately after synchronous patching, while host resolution + connect() happen in a detached .then()/.catch() chain. Functionally fine for the "don't miss early requests" goal, but the public JSDoc example (await DevConnect.init({ appName: 'MyApp' })) no longer implies "connected", which could surprise consumers who gate other logic (e.g. DevConnect.isConnected()) on the awaited call.

📝 Suggested docstring clarification
   /**
    * Initialize DevConnect.
    *
+   * Note: this resolves as soon as interceptors are patched — host
+   * detection and the WebSocket connection happen asynchronously in the
+   * background afterwards, so `DevConnect.isConnected()` may still be
+   * `false` immediately after `await`.
+   *
    * ```typescript
🤖 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 `@client_sdks/devconnect-react-native/src/client.ts` around lines 547 - 571,
Update the public JSDoc for DevConnect.init to state that awaiting it only
completes synchronous initialization and interceptor patching; host detection
and connect() occur asynchronously afterward. Clarify that callers must not
assume DevConnect.isConnected() is true immediately after await, and reference
the init() documentation/example near the changed initialization flow.
lib/features/settings/presentation/shared/preset_dropdown.dart (1)

44-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer ColorTokens or theme colors over hardcoded hex values.

0xFF0D9488 (active teal), 0xFF1E1E2E (dark popup background), and 0xFF6B7280 (icon grey) are hardcoded, while settings_page.dart uses ColorTokens for the same surfaces. If the app's theme palette changes, these values won't adapt. Consider sourcing these from ColorTokens or Theme.of(context).

♻️ Proposed refactor using ColorTokens/theme
-        color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
+        color: isDark ? ColorTokens.darkSurface : Colors.white,
-                if (isActive)
-                  const Icon(LucideIcons.check,
-                      size: 14, color: Color(0xFF0D9488))
+                if (isActive)
+                  Icon(LucideIcons.check,
+                      size: 14, color: Theme.of(context).colorScheme.primary)
-              const Icon(LucideIcons.layers, size: 15, color: Color(0xFF6B7280)),
+              Icon(LucideIcons.layers, size: 15,
+                  color: isDark ? Colors.grey[400] : Colors.grey[600]),

Also applies to: 54-54, 64-64, 76-76, 88-88

🤖 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 `@lib/features/settings/presentation/shared/preset_dropdown.dart` at line 44,
Replace the hardcoded teal, dark popup background, and grey icon colors in the
preset dropdown’s build logic with the corresponding ColorTokens or
Theme.of(context) colors, matching the palette usage in settings_page.dart;
update all affected color assignments in the dropdown.
lib/features/settings/presentation/sections/all_events_display_section.dart (1)

16-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared section widget to eliminate duplication with DataRetentionSection.

AllEventsDisplaySection and DataRetentionSection are structurally identical — same Column layout, same Row with SizedBox(width: 100) label, same PresetDropdown wiring, same helper text padding. Only the icon, provider, and localization keys differ. A single parameterized widget would cut ~50 lines of duplicated code and prevent drift when the layout evolves.

♻️ Proposed shared widget
// retention_section.dart
class RetentionSection extends ConsumerWidget {
  final IconData icon;
  final String Function(AppLocalizations) title;
  final String Function(AppLocalizations) description;
  final String Function(AppLocalizations) helper;
  final AutoDisposeStateNotifierProvider<RetentionLimitNotifier, RetentionPreset> provider;

  const RetentionSection({
    super.key,
    required this.icon,
    required this.title,
    required this.description,
    required this.helper,
    required this.provider,
  });

  `@override`
  Widget build(BuildContext context, WidgetRef ref) {
    final preset = ref.watch(provider);
    final l10n = S.of(context);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SectionTitle(icon: icon, title: title(l10n)),
        Text(description(l10n),
            style: TextStyle(fontSize: 11, color: Colors.grey[500], height: 1.4)),
        const SizedBox(height: 14),
        Row(children: [
          SizedBox(width: 100,
              child: Text(l10n.maxItems,
                  style: TextStyle(fontSize: 13, color: Colors.grey[500]))),
          Expanded(child: PresetDropdown(
            selected: preset,
            onSelected: (p) => ref.read(provider.notifier).set(p),
          )),
        ]),
        const SizedBox(height: 6),
        Padding(padding: const EdgeInsets.only(left: 100),
            child: Text(helper(l10n),
                style: TextStyle(fontSize: 10, color: Colors.grey[600], height: 1.4))),
      ],
    );
  }
}
🤖 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 `@lib/features/settings/presentation/sections/all_events_display_section.dart`
around lines 16 - 66, Extract the duplicated layout from AllEventsDisplaySection
and DataRetentionSection into a shared parameterized ConsumerWidget, such as
RetentionSection. Pass the icon, localized title/description/helper selectors,
and the appropriate retention provider; centralize provider watching,
PresetDropdown updates, labels, spacing, and styling in the shared widget, then
replace both section implementations with configured instances.
🤖 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 `@lib/components/misc/retention_hint.dart`:
- Line 66: Add a localized `showingCountOfTotal` ARB key with count and total
placeholders in every supported locale, then update the retention hint widget to
use the generated localization accessor instead of the hardcoded `Showing $count
of $total` string.

In `@lib/core/providers/retention_provider.dart`:
- Around line 23-30: The RetentionPreset.label getter hardcodes the
non-localized “Unlimited” display text. Remove presentation labels from the enum
metadata or add a context-aware label resolver used by PresetDropdown, and
resolve RetentionPreset.unlimited through S.of(context) while preserving enum
values/names for persistence.

In `@lib/core/providers/tab_visibility_provider.dart`:
- Around line 32-34: Update _load() to preserve an intentionally persisted empty
list from disable as an empty tab set, while still falling back to
TabKey.values.toSet() when a non-empty decoded list contains no recognized tab
names. Track whether the decoded list was empty before filtering, and only apply
the “all enabled” fallback for non-empty invalid data.

In `@lib/core/utils/network_url_formatter.dart`:
- Around line 58-65: In the query parameter construction within the formatter,
remove the redundant _decode call because uri.queryParametersAll already returns
decoded values; pass each value directly to FormattedQueryParam while preserving
repeated keys and insertion order.
- Around line 68-72: Preserve explicit ports when constructing FormattedUrl in
the URL formatter: replace the host-only value with uri.authority, or add
uri.port as a separate field and update downstream detail panels and copy
formatters to use it. Ensure URLs such as localhost:8080 retain their port.

In `@lib/features/all_events/presentation/detail/network_detail.dart`:
- Around line 58-64: Update _rebuildController() to pass the current
tabLabels.length into _makeController(...) instead of relying on the default
length, and clamp the restored tab index to the valid range before creating the
TabController so index 4 cannot be used with fewer tabs.
- Around line 67-73: In _resizeControllerIfNeeded, convert the result of
_tabController.index.clamp(0, newLength - 1) to an int before passing it to
_makeController, using oldIndex.toInt() or an equivalent integer conversion.

In `@lib/features/all_events/presentation/event_row/event_row.dart`:
- Around line 144-151: Only render the 4px spacer when the network event’s via
value is fetch or xhr; update the conditional block around _ViaBadge and
SizedBox so NetworkVia.unknown does not add an empty gap, while preserving the
badge and spacing for valid via values.

In `@lib/features/all_events/presentation/header/header_bar.dart`:
- Around line 128-139: The new “Showing $count of $untrimmed” hint in the header
widget is not localized. Add a generated localization entry with placeholders
for count and untrimmed, then replace the hardcoded string in the header’s Text
widget with the corresponding S.of(context) lookup.

In `@lib/features/all_events/presentation/shared/params_tab.dart`:
- Around line 34-49: Replace the ListView.builder in the detail-panel build
method with the repository’s StableListView equivalent, preserving its padding,
itemCount, and existing itemBuilder logic that renders _ParamCell values. Ensure
the list follows the required stable-list behavior for presentation-layer lists.
- Around line 118-128: Replace the raw SelectableText in the detail-panel value
rendering with the shared TextComponent, preserving the existing text, styling,
and selectable behavior. Add the corresponding text_component.dart import and
configure TextComponent consistently with other inspector detail panels.
- Around line 39-47: The copy action in the parameter row currently uses the
joined display value, flattening repeated keys and losing URI encoding. Preserve
the raw value list from the query parameter entry, and in the copy handler
serialize the key and all values with Uri(queryParameters: ...) before writing
to the clipboard, while keeping the joined string only for display.

In `@lib/features/benchmark/provider/benchmark_providers.dart`:
- Line 114: Bound retained event data at ingestion in the notifier using a fixed
5,000-entry ceiling rather than the unbounded truncateList call. Update the
corresponding state assignments in the event handlers for
benchmark_providers.dart and the analogous providers in console_providers.dart,
state_providers.dart, network_providers.dart, and storage_providers.dart, while
preserving any separate configurable display limit.

In `@lib/features/display/provider/display_providers.dart`:
- Around line 40-42: Update DisplayNotifier.add to read the current value from
retentionLimitProvider and pass it to truncateList instead of null, ensuring
appended DisplayEntry history is capped and the shouldDrop rule runs for stored
entries.

In `@lib/features/error_inspector/presentation/header/toolbar.dart`:
- Around line 88-147: Localize the hardcoded “Showing N of M” text and remove
duplicated trimmed-display logic. Add a shared localization key and use it in
both the toolbar’s trimmed section and RetentionHint, then extract the
`isTrimmed` calculation and “Showing” text rendering into a reusable widget or
helper while preserving the toolbar-specific CountUp, error color, and
PulsingDot behavior.

In `@lib/features/network_inspector/presentation/request/request_card.dart`:
- Around line 258-260: Guard the via badge-building block with both viaLabel and
viaColor non-null checks; update the condition around the via badge in the
request card so viaColor is promoted before calling withValues, including both
badge color and border styling.
- Around line 54-67: Update the URL formatting logic around `path`, `host`, and
`displayUrl`: use `uri.authority` without inventing a default scheme, preserve
ports and other authority details, and fall back to `/` when the parsed path is
empty so host-only URLs retain a meaningful title. Ensure the existing root-path
and service-action handling in `titleText` uses this normalized path.

In `@lib/features/state_inspector/presentation/pages/state_inspector_page.dart`:
- Around line 309-321: Localize the retention counter and trimmed-total hint
currently rendered in the State Inspector page. Add localized formatter methods
for the capped count and “Showing …” text, including the preset’s “Unlimited”
label, then replace the direct interpolated strings in the relevant Text widgets
with these formatters.

---

Outside diff comments:
In `@lib/features/all_events/presentation/pages/all_events_page.dart`:
- Around line 409-458: Replace the ListView.custom in the event list with
StableListView, preserving the existing controller, item extent, item builder
logic, keys, and visible item count through StableListView’s corresponding API.
Ensure the stable delegate behavior remains active during live updates.

In `@lib/l10n/app_zh_CN.arb`:
- Around line 170-173: Remove the duplicate noNetworkRequests and
apiCallsAppearHere entries from the ARB resource, retaining only one definition
of each key and its existing value.

In `@lib/l10n/app_zh_TW.arb`:
- Around line 170-173: Remove the duplicated `noNetworkRequests` and
`apiCallsAppearHere` entries from the localization resource, keeping only one
occurrence of each key and preserving their existing values.

---

Nitpick comments:
In `@client_sdks/devconnect-react-native/src/client.ts`:
- Around line 547-571: Update the public JSDoc for DevConnect.init to state that
awaiting it only completes synchronous initialization and interceptor patching;
host detection and connect() occur asynchronously afterward. Clarify that
callers must not assume DevConnect.isConnected() is true immediately after
await, and reference the init() documentation/example near the changed
initialization flow.

In
`@lib/features/network_inspector/presentation/request/request_detail_panel.dart`:
- Around line 774-783: Add a shared decodeUrlSafe(String) helper to
network_url_formatter.dart that returns Uri.decodeFull(url) with the original
URL as fallback on decoding errors. Replace the local _decoded implementation
and duplicated inline decoding in event_row.dart's _tooltipFor and
network_detail.dart with this helper, updating imports and preserving existing
behavior.
- Around line 219-270: Replace the inline via badge Builder in the request
detail panel with a shared ViaBadge widget extracted from _ViaBadge in
event_row.dart. Centralize the NetworkVia fetch/xhr color and label mapping
there, supporting any required size or padding parameters, and update both call
sites to use it so labels and styling cannot diverge.
- Around line 93-114: Move the tab-controller length synchronization out of
build and into didUpdateWidget, comparing oldWidget.entry.url with
widget.entry.url before rebuilding. Reuse the existing _rebuildController
pattern to dispose and recreate _tabController, including clamped index
preservation and setState as appropriate; keep build focused on deriving tab
labels and rendering descendants without mutating controller state.
- Around line 201-217: The selectable URL currently uses raw SelectableText
instead of the panel’s standard TextComponent. Replace SelectableText in the URL
row with TextComponent, preserving the decoded URL, styling, padding, and
max-line behavior while relying on TextComponent’s default selectable support.

In `@lib/features/network_inspector/presentation/shared/params_tab.dart`:
- Around line 107-129: The selectable value in the params tab currently uses
Flutter’s SelectableText instead of the required TextComponent. In the
value-cell widget near widget.keyName and widget.value, replace SelectableText
with TextComponent while preserving the existing value, styling, and
selectable-text behavior.

In `@lib/features/settings/presentation/sections/all_events_display_section.dart`:
- Around line 16-66: Extract the duplicated layout from AllEventsDisplaySection
and DataRetentionSection into a shared parameterized ConsumerWidget, such as
RetentionSection. Pass the icon, localized title/description/helper selectors,
and the appropriate retention provider; centralize provider watching,
PresetDropdown updates, labels, spacing, and styling in the shared widget, then
replace both section implementations with configured instances.

In `@lib/features/settings/presentation/shared/preset_dropdown.dart`:
- Line 44: Replace the hardcoded teal, dark popup background, and grey icon
colors in the preset dropdown’s build logic with the corresponding ColorTokens
or Theme.of(context) colors, matching the palette usage in settings_page.dart;
update all affected color assignments in the dropdown.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a885797c-cec2-49ee-87d1-ac8c027eea2d

📥 Commits

Reviewing files that changed from the base of the PR and between fde8f1b and 60a08dc.

⛔ Files ignored due to path filters (3)
  • macos/Podfile is excluded by none and included by none
  • macos/Runner.xcodeproj/project.pbxproj is excluded by none and included by none
  • test_manual/test_url.dart is excluded by none and included by none
📒 Files selected for processing (55)
  • client_sdks/devconnect-react-native/src/client.ts
  • lib/components/misc/retention_hint.dart
  • lib/core/providers/retention_provider.dart
  • lib/core/providers/tab_visibility_provider.dart
  • lib/core/theme/theme_provider.dart
  • lib/core/utils/list_retention.dart
  • lib/core/utils/network_url_formatter.dart
  • lib/core/utils/retention_capped.dart
  • lib/features/all_events/presentation/detail/error_detail.dart
  • lib/features/all_events/presentation/detail/event_detail_panel.dart
  • lib/features/all_events/presentation/detail/log_detail.dart
  • lib/features/all_events/presentation/detail/network_detail.dart
  • lib/features/all_events/presentation/event_row/event_row.dart
  • lib/features/all_events/presentation/header/header_bar.dart
  • lib/features/all_events/presentation/pages/all_events_page.dart
  • lib/features/all_events/presentation/shared/params_tab.dart
  • lib/features/all_events/provider/all_events_provider.dart
  • lib/features/benchmark/provider/benchmark_providers.dart
  • lib/features/console/presentation/header/toolbar.dart
  • lib/features/console/presentation/shared/count_pill.dart
  • lib/features/console/provider/console_providers.dart
  • lib/features/display/provider/display_providers.dart
  • lib/features/error_inspector/presentation/header/toolbar.dart
  • lib/features/error_inspector/provider/error_providers.dart
  • lib/features/network_inspector/presentation/pages/network_inspector_page.dart
  • lib/features/network_inspector/presentation/request/request_card.dart
  • lib/features/network_inspector/presentation/request/request_detail_panel.dart
  • lib/features/network_inspector/presentation/shared/params_tab.dart
  • lib/features/network_inspector/presentation/toolbar/toolbar.dart
  • lib/features/network_inspector/provider/network_providers.dart
  • lib/features/performance/provider/performance_providers.dart
  • lib/features/settings/presentation/pages/settings_page.dart
  • lib/features/settings/presentation/sections/all_events_display_section.dart
  • lib/features/settings/presentation/sections/data_retention_section.dart
  • lib/features/settings/presentation/shared/preset_dropdown.dart
  • lib/features/state_inspector/presentation/pages/state_inspector_page.dart
  • lib/features/state_inspector/provider/state_providers.dart
  • lib/features/storage_viewer/presentation/header/toolbar.dart
  • lib/features/storage_viewer/provider/storage_providers.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_fr.dart
  • lib/l10n/app_localizations_ja.dart
  • lib/l10n/app_localizations_vi.dart
  • lib/l10n/app_localizations_zh.dart
  • lib/l10n/app_vi.arb
  • lib/l10n/app_zh.arb
  • lib/l10n/app_zh_CN.arb
  • lib/l10n/app_zh_TW.arb
  • lib/l10n/untranslated.txt
  • lib/models/network/network_entry.dart
  • lib/server/ws_message_handler.dart
💤 Files with no reviewable changes (1)
  • lib/features/network_inspector/presentation/pages/network_inspector_page.dart

if (isTrimmed) ...[
const SizedBox(height: 2),
Text(
'Showing $count of $total',

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

Localize the "Showing N of M" string.

The Showing $count of $total text is user-facing but hardcoded in English. This PR adds localized strings for dataRetention, allEventsDisplay, and other retention-related labels across all supported locales, but this hint text is missed. Non-English users will see an English fragment in an otherwise localized UI.

🌐 Proposed fix to localize the hint

Add a new ARB key (e.g. showingCountOfTotal) with a placeholder:

+  "showingCountOfTotal": "Showing {count} of {total}",
+  "`@showingCountOfTotal`": {
+    "placeholders": {
+      "count": { "type": "int" },
+      "total": { "type": "int" }
+    }
+  },

Then use it in the widget:

-          Text(
-            'Showing $count of $total',
+          Text(
+            S.of(context).showingCountOfTotal(count, total),
📝 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
'Showing $count of $total',
Text(
S.of(context).showingCountOfTotal(count, total),
🤖 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 `@lib/components/misc/retention_hint.dart` at line 66, Add a localized
`showingCountOfTotal` ARB key with count and total placeholders in every
supported locale, then update the retention hint widget to use the generated
localization accessor instead of the hardcoded `Showing $count of $total`
string.

Comment on lines +23 to +30
String get label => switch (this) {
RetentionPreset.unlimited => 'Unlimited',
RetentionPreset.p100 => '100',
RetentionPreset.p500 => '500',
RetentionPreset.p1k => '1K',
RetentionPreset.p5k => '5K',
RetentionPreset.p10k => '10K',
};

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

Localize the Unlimited preset label.

PresetDropdown consumes this metadata, so non-English settings screens render “Unlimited” in English. Resolve display labels through S.of(context) in presentation while retaining enum names for persistence.

🤖 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 `@lib/core/providers/retention_provider.dart` around lines 23 - 30, The
RetentionPreset.label getter hardcodes the non-localized “Unlimited” display
text. Remove presentation labels from the enum metadata or add a context-aware
label resolver used by PresetDropdown, and resolve RetentionPreset.unlimited
through S.of(context) while preserving enum values/names for persistence.

Comment on lines +32 to +34
// Defensive: if nothing matched (corrupt data), fall back to
// "all enabled" rather than "all hidden" — better UX.
return result.isEmpty ? TabKey.values.toSet() : result;

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

Preserve an intentionally empty tab selection.

disable can persist [], but _load() converts that valid state into “all enabled” on restart. Distinguish an empty decoded list from a non-empty list containing no recognized tab names.

🤖 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 `@lib/core/providers/tab_visibility_provider.dart` around lines 32 - 34, Update
_load() to preserve an intentionally persisted empty list from disable as an
empty tab set, while still falling back to TabKey.values.toSet() when a
non-empty decoded list contains no recognized tab names. Track whether the
decoded list was empty before filtering, and only apply the “all enabled”
fallback for non-empty invalid data.

Comment on lines +58 to +65
// `uri.queryParametersAll` keeps insertion order AND preserves repeated
// keys as lists — the former matters for `?order=` style params, the
// latter for Supabase-style `?id=in.(1,2,3)` filters.
final params = <FormattedQueryParam>[];
uri.queryParametersAll.forEach((k, values) {
for (final v in values) {
params.add(FormattedQueryParam(k, _decode(v)));
}

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
cat >/tmp/check_uri.dart <<'DART'
void main() {
  final uri = Uri.parse('https://example.test/?q=a%252Fb');
  print(uri.queryParametersAll['q']);
}
DART
dart /tmp/check_uri.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file outline ==\n'
ast-grep outline lib/core/utils/network_url_formatter.dart --view expanded || true

printf '\n== relevant source slice ==\n'
nl -ba lib/core/utils/network_url_formatter.dart | sed -n '1,220p'

printf '\n== search for helper usages ==\n'
rg -n "_decode|queryParametersAll|FormattedQueryParam|formatUrlOneLine" lib/core/utils/network_url_formatter.dart lib/core -S || true

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 318


🌐 Web query:

Dart Uri.queryParametersAll decoded values queryParametersAll documentation

💡 Result:

In Dart, the Uri.queryParametersAll property is a getter that returns a Map<String, List> containing the URI's query parameters [1][2]. Key characteristics of this property include: Decoded Values: Each key and value within the resulting map is automatically decoded [1][2]. This process follows the rules specified for HTML form submissions (FORM post) as described in the HTML 4.01 specification, section 17.13.4, which includes handling percent-encoding and converting plus signs (+) into spaces [1][2][3]. Handling Multiple Values: Unlike the queryParameters property—which returns a Map<String, String> and maps each key to a single, arbitrary value if a key appears multiple times—queryParametersAll maps every key to a list of all its corresponding values [1][4]. If a key appears only once, its value is represented as a singleton list [1][2]. Edge Cases: If a key occurs in the query string without an assigned value, it is mapped to an empty string ("") within the list [1][2]. If the URI contains no query, the returned map is empty [1][2]. Unmodifiable: The returned map and the lists contained within it are unmodifiable [1][2]. Example Usage: final uri = Uri.parse('https://example.com/api/fetch?limit=10&limit=20&max=100'); print(uri.queryParametersAll); // {"limit":["10","20"],"max":["100"]}

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== locate file ==\n'
git ls-files | rg '^lib/core/utils/network_url_formatter\.dart$|network_url_formatter\.dart$' || true

printf '\n== print file with line numbers if present ==\n'
python3 - <<'PY'
from pathlib import Path
p = Path('lib/core/utils/network_url_formatter.dart')
print('exists:', p.exists())
if p.exists():
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if 1 <= i <= 220:
            print(f"{i:4d}: {line}")
PY

printf '\n== search decode helper in repo ==\n'
rg -n "String _decode|_decode\\(" lib/core -S || true

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 7669


Avoid decoding queryParametersAll values twice in lib/core/utils/network_url_formatter.dart:62-64. Uri.queryParametersAll already returns decoded components, so _decode(v) can turn %252F into /. Keep v as-is here.

🤖 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 `@lib/core/utils/network_url_formatter.dart` around lines 58 - 65, In the query
parameter construction within the formatter, remove the redundant _decode call
because uri.queryParametersAll already returns decoded values; pass each value
directly to FormattedQueryParam while preserving repeated keys and insertion
order.

Comment on lines +68 to +72
return FormattedUrl(
host: uri.host.isEmpty ? null : uri.host,
path: uri.path.isEmpty ? '/' : uri.path,
queryParams: params,
raw: trimmed,

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
cat >/tmp/check_authority.dart <<'DART'
void main() {
  final uri = Uri.parse('http://localhost:8080/api');
  print('host=${uri.host}, port=${uri.port}, authority=${uri.authority}');
}
DART
dart /tmp/check_authority.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== network_url_formatter ==\n'
sed -n '1,220p' lib/core/utils/network_url_formatter.dart

printf '\n== find FormattedUrl usages ==\n'
rg -n "FormattedUrl|formattedUrl|authority|uri\.host|uri\.port" lib -S

printf '\n== locate detail panels mentioning url ==\n'
rg -n "detail panel|copy.*url|copy.*URL|FormattedUrl|raw:" lib -S

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 17178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo root:"
pwd
echo

echo "files:"
git ls-files | rg '^lib/core/utils/network_url_formatter\.dart$|^lib/.*formatted_url|^lib/.*detail.*panel|^lib/.*url' -n

echo
echo "network_url_formatter:"
sed -n '1,220p' lib/core/utils/network_url_formatter.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 6878


🌐 Web query:

Dart Uri.host Uri.authority port difference documentation

💡 Result:

In Dart, the Uri class distinguishes between authority, host, and port as follows: The authority component represents the hierarchical part of the URI that includes the user information, host, and port [1][2]. It is a single string that aggregates these parts [1]. The host is a component of the authority that specifies the hostname, IPv4 address, or IPv6 address [3][2]. The port is an integer component of the authority that specifies the port number [4]. A key distinction is that the port property always returns an integer [4]. If no port is explicitly provided in the URI string, the port property returns the default port for the scheme (80 for http, 443 for https, or 0 for all other schemes) [4][2]. You can use the hasPort property to determine if the port was explicitly specified in the original URI [5]. In summary, while authority is the concatenated string representation of the authority section, host and port are the specific, parsed components of that section [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== request_card ==\n'
sed -n '1,140p' lib/features/network_inspector/presentation/request/request_card.dart

printf '\n== request_detail_panel url/copy area ==\n'
sed -n '740,840p' lib/features/network_inspector/presentation/request/request_detail_panel.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 8589


Preserve the full authority here. uri.host drops explicit ports, so URLs like http://localhost:8080/api get rendered without :8080 in the downstream detail panels and copy formatters. Store uri.authority or keep port alongside host.

🤖 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 `@lib/core/utils/network_url_formatter.dart` around lines 68 - 72, Preserve
explicit ports when constructing FormattedUrl in the URL formatter: replace the
host-only value with uri.authority, or add uri.port as a separate field and
update downstream detail panels and copy formatters to use it. Ensure URLs such
as localhost:8080 retain their port.

Comment thread lib/features/display/provider/display_providers.dart
Comment on lines +88 to +147
final isTrimmed =
retentionLimit != null && capped.total > count;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (count > 0)
Padding(
padding: const EdgeInsets.only(right: 4),
child: PulsingDot(
color: ColorTokens.logError,
size: 7,
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (count > 0)
Padding(
padding: const EdgeInsets.only(right: 4),
child: PulsingDot(
color: ColorTokens.logError,
size: 7,
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: count > 0
? ColorTokens.logError.withValues(alpha: 0.12)
: (isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(10),
),
child: CountUp(
value: count,
formatter: (n) => retentionLimit == null
? '$n'
: '$n / $retentionLabel',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
fontFamily: AppConstants.monoFontFamily,
color: count > 0
? ColorTokens.logError
: (isDark
? Colors.grey[400]
: Colors.grey[600]),
),
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: count > 0
? ColorTokens.logError.withValues(alpha: 0.12)
: (isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.black.withValues(alpha: 0.04)),
borderRadius: BorderRadius.circular(10),
),
child: CountUp(
value: count,
],
),
if (isTrimmed) ...[
const SizedBox(height: 2),
Text(
'Showing $count of ${capped.total}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
fontSize: 9,
fontFamily: AppConstants.monoFontFamily,
color: count > 0
? ColorTokens.logError
: (isDark
? Colors.grey[400]
: Colors.grey[600]),
color: isDark ? Colors.grey[600] : Colors.grey[500],
),
),
),
],

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

Localize the "Showing N of M" string and consider extracting shared logic.

Line 140 hardcodes 'Showing $count of ${capped.total}' in English. Since this PR adds localization keys for other new retention strings, this user-facing text should also be localized. The same hardcoded pattern exists in RetentionHint, so both would need the new key.

Additionally, the isTrimmed check and "Showing N of M" rendering duplicate RetentionHint's logic. While the error toolbar justifiably needs CountUp animation, ColorTokens.logError, and PulsingDot, extracting the trimmed-text portion into a small shared widget would prevent future drift.

🤖 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 `@lib/features/error_inspector/presentation/header/toolbar.dart` around lines
88 - 147, Localize the hardcoded “Showing N of M” text and remove duplicated
trimmed-display logic. Add a shared localization key and use it in both the
toolbar’s trimmed section and RetentionHint, then extract the `isTrimmed`
calculation and “Showing” text rendering into a reusable widget or helper while
preserving the toolbar-specific CountUp, error color, and PulsingDot behavior.

Comment on lines +54 to 67
final path = uri?.path ?? entry.url;
// Prefix scheme so the user can read the request at a glance —
// matches what curl-style tooling shows.
final host = uri == null
? ''
: '${uri.scheme.isEmpty ? 'https' : uri.scheme}://${uri.host}';
// Card title is the path (compact) — the full scheme+host lives
// in the badge row below, so we don't repeat it on the title.
final displayUrl = path;
final formatted = parseFormattedUrl(entry.url);
final isRootPath = path == '/' || path.isEmpty;
final titleText = (entry.serviceAction != null && isRootPath)
? entry.serviceAction!
: displayUrl;

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

# Inspect the target file and nearby context with line numbers.
file='lib/features/network_inspector/presentation/request/request_card.dart'
wc -l "$file"
sed -n '1,220p' "$file" | cat -n

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 10604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='lib/features/network_inspector/presentation/request/request_card.dart'

# Show the rest of the widget around host/path usage and any null-sensitive code.
sed -n '220,373p' "$file" | cat -n

# Probe Dart Uri behavior on representative inputs without modifying repo files.
python3 - <<'PY'
from urllib.parse import urlparse

cases = [
    "example.com",
    "/foo",
    "https://example.com",
    "https://example.com:8080/path",
    "http://example.com",
    "example.com/path",
]
for s in cases:
    u = urlparse(s)
    print(s, "=>", {"scheme": u.scheme, "netloc": u.netloc, "path": u.path, "authority": u.netloc})
PY

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 9502


🌐 Web query:

Dart Uri.parse relative URL host authority path behavior docs

💡 Result:

In Dart, the Uri.parse method is designed to parse URI strings according to RFC 3986 [1][2]. A key behavior of the Uri class is that it is normalizing, meaning it does not preserve semantically equivalent but non-canonical string representations [3]. When you use Uri.parse on a relative URL: 1. Normalization: The Uri class automatically normalizes the path [3]. For instance, leading "./" segments are removed because they are considered equivalent to the path without them [3]. This means that if you parse "./foo", the resulting Uri object will represent it as "foo" [3]. 2. Authority and Host: If a URI string lacks a scheme and authority, Uri.parse interprets it as a relative URI reference [1][2]. If the string does not start with a slash, it is treated as a path-only relative URI [4][2]. 3. Resolving Relative URIs: To resolve a relative URI against a base URI, use the resolve or resolveUri methods [5][2]. The resolution process follows the "Transform Reference" algorithm described in RFC 3986 [4][1]. Specifically, the resolveUri method is updated to handle cases where the base URI is a relative path (i.e., no scheme, no authority, and the path does not start with a slash), combining paths without stripping leading ".." segments [4][2]. 4. Distinction: Because the Uri class is designed for semantic URI handling rather than unstructured string storage, it is not possible to preserve distinguishing features like leading "./" in the Uri object itself [3]. If your application requires preserving these distinctions for system-level lookups, you should manage them as strings before parsing them into Uri objects [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='lib/features/network_inspector/presentation/request/request_card.dart'
sed -n '220,330p' "$file" | cat -n

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 6972


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='lib/features/network_inspector/presentation/request/request_card.dart'

# Find the host rendering and any parseFormattedUrl helpers used in this widget.
rg -n "formatted|host|parseFormattedUrl|serviceAction|queryHint" "$file"

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 1004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='lib/features/network_inspector/presentation/request/request_card.dart'

sed -n '220,330p' "$file" | cat -n
rg -n "host|authority|path|displayUrl|titleText|serviceAction" "$file"

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 7764


Don’t invent a scheme or drop the authority here. Uri.parse('example.com') and //example.com:8080/path both end up with a bad https:// host and the port gets lost. Use uri.authority for the host row, and fall back to / when uri.path is empty so host-only URLs still have a title.

🤖 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 `@lib/features/network_inspector/presentation/request/request_card.dart` around
lines 54 - 67, Update the URL formatting logic around `path`, `host`, and
`displayUrl`: use `uri.authority` without inventing a default scheme, preserve
ports and other authority details, and fall back to `/` when the parsed path is
empty so host-only URLs retain a meaningful title. Ensure the existing root-path
and service-action handling in `titleText` uses this normalized path.

Comment on lines +258 to +260
color: viaColor!.withValues(alpha: 0.10),
border: Border.all(
color: viaColor.withValues(alpha: 0.22),

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
dart analyze lib/features/network_inspector/presentation/request/request_card.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline lib/features/network_inspector/presentation/request/request_card.dart --view expanded || true

# Show the relevant area with line numbers
wc -l lib/features/network_inspector/presentation/request/request_card.dart
sed -n '220,290p' lib/features/network_inspector/presentation/request/request_card.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 4416


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the definitions and nearby logic for viaLabel and viaColor
rg -n -C 4 "viaLabel|viaColor" lib/features/network_inspector/presentation/request/request_card.dart

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 2468


Guard viaColor before building the via badge. viaLabel != null doesn’t promote viaColor, so viaColor.withValues(...) at line 260 still violates null-safety; gate the block with viaColor != null too.

🤖 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 `@lib/features/network_inspector/presentation/request/request_card.dart` around
lines 258 - 260, Guard the via badge-building block with both viaLabel and
viaColor non-null checks; update the condition around the via badge in the
request card so viaColor is promoted before calling withValues, including both
badge color and border styling.

Comment on lines +309 to +321
Text(
retentionLimit == null
? '$c changes'
: '$c / $retentionLabel changes',
style: theme.textTheme.bodySmall,
),
if (isTrimmed)
Text(
'Showing $c of ${capped.total}',
style: TextStyle(
fontSize: 9,
fontFamily: AppConstants.monoFontFamily,
color: isDark ? Colors.grey[600] : Colors.grey[500],

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

Localize the new retention counter text.

The newly rendered count and “Showing …” text are English-only, including the Unlimited label supplied by the preset. This leaves the State Inspector partially untranslated in every non-English locale. Add localized formatters for the capped count and trimmed-total hint.

🤖 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 `@lib/features/state_inspector/presentation/pages/state_inspector_page.dart`
around lines 309 - 321, Localize the retention counter and trimmed-total hint
currently rendered in the State Inspector page. Add localized formatter methods
for the capped count and “Showing …” text, including the preset’s “Unlimited”
label, then replace the direct interpolated strings in the relevant Text widgets
with these formatters.

@buivietphi
buivietphi merged commit 8cb710b into main Jul 11, 2026
5 checks passed
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.

1 participant