ui_commenter

A debug-only, in-app tool for any Flutter project. Tap any widget in the running app, see it highlighted, type a comment, and copy a ready-to-paste prompt describing the widget (source location, type, key, ancestry) plus your comment — then paste it into a coding agent (Claude Code, Codex, etc.) to drive a change against that exact widget.

It's the Flutter equivalent of the web "click-to-component + comment" tooling: the overlay hit-tests the render tree (like document.elementFromPoint) and reads each widget's compiler-injected creation location (like a data-source attribute), exposed via --track-widget-creation (the default in debug).

Why

Pointing a coding agent at "that button" usually means hunting for the file and line yourself. ui_commenter closes that loop from inside the running app: tap the widget, say what you want, paste the result. The prompt already carries the exact file:line:column, so the agent edits the right widget on the first try.

Install

ui_commenter is a debug-only dev tool, so add it under dev_dependencies:

dev_dependencies:
  ui_commenter: ^0.1.0

Usage

Wrap your app content once. The natural seam is MaterialApp.builder, so the commenter sits above the app's Navigator/Overlay and can highlight anything beneath it:

import 'package:ui_commenter/ui_commenter.dart';

MaterialApp(
  builder: (context, child) => UiCommenter(
    enabled: kMyDevFlag, // your own debug-menu / --dart-define toggle
    config: const UiCommenterConfig(appName: 'Acme Flutter app'),
    child: child!,
  ),
  // ...
);

That is the only required host-app change. No analyzer suppressions, build flags (beyond the default --track-widget-creation), or folder conventions are imposed on your project.

A runnable demo lives in example/.

At runtime (debug build)

  1. With enabled on, a small draggable FAB appears — tap it to arm select mode.
  2. Tap a widget → it's highlighted, a comment is appended to the list, and a sheet slides up.
  3. Type your comment. Tap more widgets to accumulate more; prev/next navigate the list with an INDEX/TOTAL indicator, and edits survive navigation.
  4. Copy Prompt writes a single prompt covering every comment to the clipboard and clears the list. Paste it into your coding agent.

Clear All empties the list; the per-comment trash removes just the current one; tapping outside hides the sheet (dropping the current comment only if its field is empty).

Widget inspector

The identity frame in the comment sheet carries a small inspect button (ruler icon, top-right). Tapping it opens a read-only Widget Inspector sheet for the selected widget — an in-app analogue of the DevTools Layout Explorer:

  • a box diagram of the widget nested in its parent, with per-side padding and the widget's own w/h;
  • the geometry (size, global offset, incoming constraints, parent offset, flex, render-object type);
  • a default-aware property table (debugFillProperties output, with a default badge on properties left at their default);
  • copy shortcuts for the source location and the property dump.

It is purely diagnostic — it produces no prompt output and changes nothing about the capture/copy pipeline. Geometry needs a laid-out RenderBox; rich properties and source locations need debug + --track-widget-creation. When any of those is missing the sheet degrades gracefully (identity-only). Like the rest of the tool it is debug-only and never reaches release builds.

Configuration

UiCommenterConfig keeps the tool reusable without forking. Every field has a sensible default, so const UiCommenterConfig() works out of the box.

Field Default Purpose
appName generic phrasing Woven into the default prompt header. Ignored when promptBuilder is set.
promptBuilder built-in formatter String Function(List<WidgetComment>) — full override of the copied prompt text.
highlightColor Color(0xFF2196F3) Color of the selection highlight box and select-mode tint.
localPathPredicate built-in heuristic bool Function(String absolutePath) — override "is this file part of my project?" for monorepos/melos.
maxAncestors 4 Cap on the ancestor chain length in the prompt.
brightness null (follow app) Force the tool UI's Brightness independent of the host app.

Theming

By default the tool UI follows your app's theme — it reads Material color roles (colorScheme.surface, inverseSurface, error, …), so it renders light in a light app and dark in a dark app, and flips with themeMode. Only the selection accent is fixed, via highlightColor.

To pin the tool's appearance regardless of the app (e.g. always-dark debug chrome), set brightness. It re-themes only the commenter overlay; your app subtree keeps its own theme.

UiCommenterConfig(brightness: Brightness.dark);

Exported types

For a custom promptBuilder, the package exports two read-only data types:

  • SelectedWidget — immutable capture of one tapped widget: file?, line?, column?, type, key?, ancestry, bounds, plus hasLocation and relativeLocation helpers.
  • WidgetComment — a SelectedWidget paired with the typed text.
UiCommenterConfig(
  promptBuilder: (comments) => comments
      .map((c) => '${c.widget.relativeLocation}: ${c.text}')
      .join('\n'),
);

Monorepo / melos layouts

The built-in heuristic treats a lib/ Dart source as "local" unless it is the Flutter SDK, a pub package, or ui_commenter itself. If your project's code lives across several packages (melos), tell the tool which paths count as yours:

UiCommenterConfig(
  localPathPredicate: (absolutePath) =>
      absolutePath.contains('/packages/') && absolutePath.contains('/lib/'),
);

Output prompt

The default formatter produces, for multiple comments, a single prompt with a count header and one numbered block per widget:

You are editing the Acme Flutter app. The developer selected 2 widgets in the
running app and left a comment about each.

Widget 1:
- File: lib/features/history/presentation/home_summary_widget.dart:128:14
- Type: ElevatedButton
- Key:  ValueKey('refresh')
- Path: HomeScreen › HomeSummaryWidget › Card › ElevatedButton
- Comment: Make this button full-width and use the primary color.

Widget 2:
- File: lib/features/history/presentation/home_screen.dart:236:25
- Type: Text
- Path: HomeScreen › Column › Text
- Comment: Increase the font size and bold it.

Debug-only & --track-widget-creation

  • The overlay is mounted only when enabled && kDebugMode. In any release or profile build, UiCommenter returns its child untouched and the rest is tree-shaken out — zero impact on shipped apps. enabled is your own dev flag (a --dart-define, flavor constant, or debug-menu toggle); the default true is safe because the kDebugMode gate already blocks release leakage.
  • Source locations come from --track-widget-creation, which flutter run enables by default in debug. Without it (or in profile/release), the tool degrades gracefully: it still emits type + key + ancestry, noting that the source location is unavailable.
  • Monorepos / melos: the built-in "is this file local?" heuristic keys off a single lib/. If your local code lives outside one lib/, supply config.localPathPredicate to classify it yourself.

Supported platforms

Platform Status
iOS / Android Supported (simulators and devices), debug builds.
macOS Supported — verified to build and run from the example app.
Windows Supported — desktop runner scaffolded; same platform-agnostic capture path as macOS.
Web / Linux Not verified. Web is the main unknown: --track-widget-creation and creation-location paths differ under the web toolchain, so the File: line may not resolve.

The capture pipeline uses only platform-agnostic framework symbols, so desktop behaves like mobile. The one platform-specific concern is path formatting: Windows creation paths use backslashes, which the local-file heuristic and the emitted File: location both normalize. Non-standard layouts can still override classification via config.localPathPredicate.

Manual verification

Unit/widget tests cover formatting, path classification, hit testing, and the overlay's list/navigation behavior. The end-to-end capture — that a tap on a real lib/** widget yields the correct File: *.dart:line:col — depends on --track-widget-creation and is best confirmed on a simulator:

cd example
flutter run            # debug; track-widget-creation is on by default
flutter run -d macos   # or run on the desktop runner

Then arm select mode, tap a widget, and check the File: line in the sheet points at the right source. The package is built and tested against Flutter 3.38.x / Dart 3.10.x.

Flutter version coupling

The capture pipeline relies on a few framework symbols that are stable but not part of the public stable API surface — re-verify these on a Flutter upgrade:

  • RenderObject.debugCreatorDebugCreator.element
  • WidgetInspectorService.instance.selection.currentElement
  • WidgetInspectorService.instance.getSelectedWidget (@protected; called behind one scoped // ignore: invalid_use_of_protected_member)

They are touched in only two files: lib/src/creation_location.dart and lib/src/widget_locator.dart.

Contributing

The design and rationale live in docs/ui-commenter.md. Run flutter test and flutter analyze before opening a PR.

License

MIT

Libraries

ui_commenter
Debug-only, in-app widget commenter.