ui_commenter 0.1.1
ui_commenter: ^0.1.1 copied to clipboard
Debug-only Flutter tool: tap any widget in the running app, comment on it, and copy a ready-to-paste prompt (source location, type, key, ancestry) for a coding agent.
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) #
- With
enabledon, a small draggable FAB appears — tap it to arm select mode. - Tap a widget → it's highlighted, a comment is appended to the list, and a sheet slides up.
- Type your comment. Tap more widgets to accumulate more; prev/next navigate
the list with an
INDEX/TOTALindicator, and edits survive navigation. - 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 (
debugFillPropertiesoutput, with adefaultbadge 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, plushasLocationandrelativeLocationhelpers.WidgetComment— aSelectedWidgetpaired with the typedtext.
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,UiCommenterreturns itschilduntouched and the rest is tree-shaken out — zero impact on shipped apps.enabledis your own dev flag (a--dart-define, flavor constant, or debug-menu toggle); the defaulttrueis safe because thekDebugModegate already blocks release leakage. - Source locations come from
--track-widget-creation, whichflutter runenables 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 onelib/, supplyconfig.localPathPredicateto 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.debugCreator→DebugCreator.elementWidgetInspectorService.instance.selection.currentElementWidgetInspectorService.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.