Skip to content

Add action-based removal methods for various providers#49

Merged
SunkenInTime merged 4 commits intomainfrom
out-of-bounds-undo-redo
Mar 16, 2026
Merged

Add action-based removal methods for various providers#49
SunkenInTime merged 4 commits intomainfrom
out-of-bounds-undo-redo

Conversation

@SunkenInTime
Copy link
Owner

  • Implemented removeAbilityAsAction, removeAgentAsAction, removeImageAsAction, removeTextAsAction, and removeUtilityAsAction methods in their respective providers to handle deletions with action logging.
  • Updated the deleteHoveredTarget and deletePlacedWidget functions to utilize the new action-based removal methods, enhancing the consistency of deletion operations across the application.
  • Improved the debug logging system by adding repeat handling and a badge for repeated log entries in the debug dialog.

These changes enhance the application's ability to track user actions and improve the overall debugging experience.

- Implemented `removeAbilityAsAction`, `removeAgentAsAction`, `removeImageAsAction`, `removeTextAsAction`, and `removeUtilityAsAction` methods in their respective providers to handle deletions with action logging.
- Updated the `deleteHoveredTarget` and `deletePlacedWidget` functions to utilize the new action-based removal methods, enhancing the consistency of deletion operations across the application.
- Improved the debug logging system by adding repeat handling and a badge for repeated log entries in the debug dialog.

These changes enhance the application's ability to track user actions and improve the overall debugging experience.
Copilot AI review requested due to automatic review settings March 16, 2026 20:35
@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
icarus Ready Ready Preview, Comment Mar 16, 2026 8:59pm

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR refactors deletion operations across all widget providers by introducing removeXAsAction methods that encapsulate the existence check and action logging alongside the actual removal, then updates every call site to use these new methods. It also adds repeat-deduplication to the debug log system (merging adjacent identical entries into a single badged entry with a repeat count) and a responsive agent icon row in the strategy tile.

Key changes:

  • removeAbilityAsAction, removeAgentAsAction, removeImageAsAction, removeTextAsAction, and removeUtilityAsAction added to their respective providers, each guarding with an existence check and logging a UserAction before delegating to the original removeX method.
  • deleteHoveredTarget and deletePlacedWidget in delete_helpers.dart are greatly simplified by delegating to these new methods — however, the DeleteTargetType.lineup branch dropped its existence guard without gaining one inside deleteLineUpById, leaving a potential StateError crash.
  • InAppDebugProvider.addEntry now merges a new entry into the previous one if they share the same dedupeKey, incrementing repeatCount and updating lastOccurredAt. The debug dialog surfaces this with a _RepeatBadge.
  • AppErrorReporter gains a per-key toast cooldown (_interactiveNotificationCooldown = 3s) to avoid spamming the user with repeated toasts for the same error. Three helper functions (_buildNotificationFingerprint, _normalizeFingerprintFragment, _firstStackTraceLine) duplicate logic already present in in_app_debug_provider.dart.
  • StrategyTileDetails now uses a LayoutBuilder-driven _DynamicAgentRow that calculates the correct number of visible agent icons from the available width, replacing the previous hard-coded _maxVisibleAgents = 3 cap.

Confidence Score: 3/5

  • Mostly safe to merge after fixing the lineup deletion regression, which can throw a StateError at runtime.
  • The majority of changes are clean refactors that correctly encapsulate action logging. The one concrete regression is in delete_helpers.dart: the lineup branch lost its existence guard without a replacement, and deleteLineUpById calls firstWhere without orElse, so passing a stale or already-removed ID will throw a StateError. The duplicated fingerprint helpers in AppErrorReporter are a maintainability concern but not a runtime bug.
  • lib/widgets/delete_helpers.dart — the DeleteTargetType.lineup branch needs an existence guard restored. lib/services/app_error_reporter.dart — duplicated deduplication key helpers should be consolidated.

Important Files Changed

Filename Overview
lib/widgets/delete_helpers.dart Simplified deletion logic by delegating to new removeXAsAction methods; the lineup case regresses by dropping its existence guard without a replacement, leaving deleteLineUpById exposed to a throwing firstWhere call.
lib/services/app_error_reporter.dart Adds interactive-notification cooldown and per-key deduplication, but duplicates three helper functions already present in in_app_debug_provider.dart; the explicit dedupeKey argument passed to DebugLogEntry is also redundant.
lib/providers/in_app_debug_provider.dart Adds repeat-merging for adjacent identical log entries with dedupeKey, repeatCount, and lastOccurredAt; logic is correct and the _maxEntries cap still applies to non-merged entries.
lib/providers/ability_provider.dart Adds removeAbilityAsAction with an existence guard that logs a UserAction before delegating to removeAbility; clean and consistent with sibling providers.
lib/widgets/dialogs/in_app_debug_dialog.dart Adds a _RepeatBadge widget and "Latest occurrence" label for merged log entries; UI changes are straightforward with no logic issues.
lib/widgets/strategy_tile/strategy_tile_sections.dart Replaces fixed 3-agent cap with a responsive _DynamicAgentRow using LayoutBuilder; contains a leftover commented-out mainAxisAlignment line.

Sequence Diagram

sequenceDiagram
    participant UI as UI / placed_widget_builder
    participant DH as delete_helpers
    participant XP as XxxProvider (ability/agent/text/image/utility)
    participant AP as ActionProvider
    participant LP as LineUpProvider

    Note over UI,LP: deleteHoveredTarget / deletePlacedWidget

    UI->>DH: deleteHoveredTarget(ref, target)
    alt target.type != lineup
        DH->>XP: removeXAsAction(id)
        XP-->>XP: guard: state.any(id)?
        XP->>AP: addAction(UserAction deletion)
        XP->>XP: removeX(id)
    else target.type == lineup (no guard ⚠️)
        DH->>LP: deleteLineUpById(id)
        LP->>AP: addAction(UserAction deletion)
        LP-->>LP: firstWhere(id) [throws if missing]
        LP->>LP: remove from state
    end

    Note over UI,LP: AppErrorReporter toast deduplication

    UI->>AppErrorReporter: reportWarning/reportError(msg)
    AppErrorReporter->>AppErrorReporter: addDebugLog → DebugLogEntry
    AppErrorReporter->>InAppDebugProvider: addEntry(entry)
    InAppDebugProvider-->>InAppDebugProvider: canMergeWith(last)?
    alt merge
        InAppDebugProvider->>InAppDebugProvider: mergeRepeat → update repeatCount & lastOccurredAt
    else new entry
        InAppDebugProvider->>InAppDebugProvider: append (cap at 500)
    end
    AppErrorReporter->>AppErrorReporter: _showInteractiveNotificationIfNeeded
    AppErrorReporter-->>AppErrorReporter: cooldown check (3s per dedupeKey)
    opt cooldown not active
        AppErrorReporter->>UI: Settings.showToast(message)
    end
Loading

Last reviewed commit: 678c57b

Comment on lines 28 to 30
case DeleteTargetType.lineup:
if (!_lineUpExists(ref, target.id)) return;
ref.read(lineUpProvider.notifier).deleteLineUpById(target.id);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing existence guard for lineup deletion

The old code protected this branch with if (!_lineUpExists(ref, target.id)) return; before calling deleteLineUpById. That guard was removed here, but unlike the other five types, no equivalent check was moved inside deleteLineUpById.

Looking at line_provider.dart line 319, deleteLineUpById calls state.lineUps.firstWhere((lineUp) => lineUp.id == id) with no orElse callback. When a non-existent ID is passed, Dart's firstWhere throws a StateError, which will crash the function call and can bubble up as an unhandled exception in the UI.

All other cases (agent, ability, text, image, utility) moved their guard into their new removeXAsAction methods. The lineup case skipped this pattern, making it inconsistent and unsafe.

Suggested change
case DeleteTargetType.lineup:
if (!_lineUpExists(ref, target.id)) return;
ref.read(lineUpProvider.notifier).deleteLineUpById(target.id);
return;
case DeleteTargetType.lineup:
if (!state.lineUps.any((l) => l.id == target.id)) return;
ref.read(lineUpProvider.notifier).deleteLineUpById(target.id);
return;

Or, alternatively, add an early-return guard inside deleteLineUpById itself (parallel to the other providers).

Copy link
Owner Author

@SunkenInTime SunkenInTime Mar 16, 2026

Choose a reason for hiding this comment

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

@greptile The thing is that you can't really remove a lineup by dragging, so it's literally impossible to get into that scenario.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair point—if lineups can't be dragged to delete in the UI, then deleteHoveredTarget won't be called for them through the normal drag-and-drop flow, making the guard unnecessary in practice.

Since this is non-obvious from the code alone, you might consider adding a brief comment to document this architectural constraint:

Suggested change
case DeleteTargetType.lineup:
if (!_lineUpExists(ref, target.id)) return;
ref.read(lineUpProvider.notifier).deleteLineUpById(target.id);
return;
case DeleteTargetType.lineup:
// Note: lineups cannot be dragged to delete zones, so this case
// only executes in controlled scenarios where existence is guaranteed
ref.read(lineUpProvider.notifier).deleteLineUpById(target.id);

But if you'd rather keep the code clean without explanatory comments, that works too—just good to know the constraint exists.

Comment on lines +186 to +219
static String _buildNotificationFingerprint({
required DebugLogLevel level,
required String message,
String? source,
String? errorText,
String? stackTrace,
}) {
return [
level.name,
_normalizeFingerprintFragment(source),
_normalizeFingerprintFragment(message),
_normalizeFingerprintFragment(errorText),
_normalizeFingerprintFragment(_firstStackTraceLine(stackTrace)),
].join('|');
}

static String _normalizeFingerprintFragment(String? value) {
if (value == null) return '';

return value.replaceAll(RegExp(r'\s+'), ' ').trim();
}

static String? _firstStackTraceLine(String? stackTrace) {
if (stackTrace == null) return null;

for (final line in stackTrace.split('\n')) {
final trimmed = line.trim();
if (trimmed.isNotEmpty) {
return trimmed;
}
}

return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicated deduplication key logic

_buildNotificationFingerprint, _normalizeFingerprintFragment, and _firstStackTraceLine here are byte-for-byte copies of _buildDebugLogDedupeKey, _normalizeDebugLogFragment, and _firstStackTraceLine in in_app_debug_provider.dart. Since AppErrorReporter.addDebugLog already constructs a DebugLogEntry (which auto-computes its own dedupeKey using the provider-side helpers), the explicit dedupeKey: argument on lines 117–123 is redundant — the constructor will derive the same key if it's omitted.

Consider removing the three private helpers from AppErrorReporter and dropping the dedupeKey: argument from the DebugLogEntry constructor call. That would eliminate the duplication and guarantee the two code paths can never drift apart if the hashing logic changes.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves UX around strategy tiles and in-app debugging, and standardizes deletion flows by routing removals through provider helpers that record undo/redo actions.

Changes:

  • Make strategy tile agent icons responsive to available width via a LayoutBuilder-driven row.
  • Deduplicate repeated debug log entries (repeat count + latest occurrence) and rate-limit “Open Logs” toasts.
  • Introduce remove*AsAction helpers across providers and update deletion call sites to consistently record deletion actions.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lib/widgets/strategy_tile/strategy_tile_sections.dart Adds a dynamic agent icon row and refactors layout constants.
lib/widgets/draggable_widgets/placed_widget_builder.dart Switches out-of-bounds removals to remove*AsAction variants.
lib/widgets/dialogs/in_app_debug_dialog.dart Displays repeat badge + latest occurrence for deduped log entries.
lib/widgets/delete_helpers.dart Simplifies deletion helpers to call provider remove*AsAction.
lib/services/app_error_reporter.dart Returns DebugLogEntry from logging, adds toast dedupe/rate limiting.
lib/providers/utility_provider.dart Adds removeUtilityAsAction helper.
lib/providers/text_provider.dart Adds removeTextAsAction helper.
lib/providers/in_app_debug_provider.dart Adds log dedupe key + repeat merge behavior.
lib/providers/image_provider.dart Adds removeImageAsAction helper.
lib/providers/agent_provider.dart Adds removeAgentAsAction helper and uses it for OOB removal.
lib/providers/ability_provider.dart Adds removeAbilityAsAction helper and uses it for OOB removal.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +117 to +123
dedupeKey: _buildNotificationFingerprint(
level: level,
message: message,
source: source,
errorText: errorText,
stackTrace: stackTrace?.toString(),
),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
SunkenInTime and others added 3 commits March 16, 2026 16:41
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…n and associated helper methods. This simplifies error reporting and reduces code complexity, enhancing maintainability.
@SunkenInTime SunkenInTime merged commit fe2fbdc into main Mar 16, 2026
1 of 3 checks passed
@SunkenInTime SunkenInTime deleted the out-of-bounds-undo-redo branch March 16, 2026 20:45
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