[roll] Update third-party dart packages

Roller-URL: https://ci.chromium.org/b/8790173535669781329
Roller-Owners: flutter-on-fuchsia-team@google.com, godofredoc@google.com
CQ-Do-Not-Cancel-Tryjobs: true
Change-Id: I01035865da12449654d131d7c83f8db5dfbfa220
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/dart-pkg/+/796942
Commit-Queue: GI Roller <global-integration-roller@fuchsia-infra.iam.gserviceaccount.com>
diff --git a/dwds/BUILD.gn b/dwds/BUILD.gn
index 80acd3c..0d85f99 100644
--- a/dwds/BUILD.gn
+++ b/dwds/BUILD.gn
@@ -1,11 +1,11 @@
-# This file is generated by package_importer.py for dwds-16.0.3
+# This file is generated by package_importer.py for dwds-17.0.0
 
 import("//build/dart/dart_library.gni")
 
 dart_library("dwds") {
   package_name = "dwds"
 
-  language_version = "2.19"
+  language_version = "3.0"
 
   disable_analysis = true
 
@@ -48,6 +48,8 @@
     "data/connect_request.g.dart",
     "data/debug_event.dart",
     "data/debug_event.g.dart",
+    "data/debug_info.dart",
+    "data/debug_info.g.dart",
     "data/devtools_request.dart",
     "data/devtools_request.g.dart",
     "data/error_response.dart",
@@ -116,6 +118,7 @@
     "src/utilities/objects.dart",
     "src/utilities/sdk_configuration.dart",
     "src/utilities/shared.dart",
+    "src/utilities/synchronized.dart",
     "src/version.dart",
     "src/web_utilities/batched_stream.dart",
   ]
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index c6eb8c4..677abae 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,12 +1,38 @@
-## 16.0.3
+## 17.0.0
 
-- Update the `vm_service` constraint to `>=10.1.0 <12.0.0`. See
-  https://github.com/dart-lang/webdev/issues/1912
+- Include debug information in the event sent from the injected client to the
+  Dart Debug Extension notifying that the Dart app is ready.
+- Fix null cast error on expression evaluations after dwds fails to find class
+  metadata.
+- Include the entire exception description up to the stacktrace in
+  `mapExceptionStackTrace`.
+- Allow enabling experiments in the expression compiler service.
+- Pre-warm expression compiler cache to speed up Flutter Inspector loading.
+- Display full error on failure to start DDS.
+- Fix crash on processing DevTools event when starting DevTools from DevTools
+  uri.
+- Prepare or Dart 3 alpha breaking changes:
+  - Move weak null safety tests to special branch of `build_web_compilers`.
+  - Do not pass `--(no)-sound-null-safety` flag to build daemon.
+- Add back `ChromeProxyService.setExceptionPauseMode()` without override.
+- Make hot restart atomic to prevent races on simultaneous execution.
+- Return error on expression evaluation if expression evaluator stopped.
+- Update SDK constraint to `>=3.0.0-134.0.dev <4.0.0`.
+- Update `package:vm_service` constraint to `>=10.1.0 <12.0.0`.
+- Fix expression compiler throwing when weak SDK summary is not found.
 
-## 16.0.2
-
-- Don't complete an already completed `Completer` in `ChromeProxyService` to fix
-  Flutter tools crash: https://github.com/dart-lang/webdev/pull/1862
+**Breaking changes**
+- Include an optional param to `Dwds.start` to indicate whether it is running
+  internally or externally.
+- Include an optional param to `Dwds.start` to indicate whether it a Flutter
+  app or not.
+- Remove deprecated `ChromeProxyService.setExceptionPauseMode()`.
+- Support dart 3.0-alpha breaking changes:
+  - Generate missing SDK assets for tests.
+  - Enable frontend server null safe tests.
+  - Update `build_web_compilers` constraint to `^4.0.0`.
+  - Update `build_runner` constraint to `^2.4.0`.
+  - Support changes in the SDK layout for dart 3.0.
 
 ## 16.0.1
 
diff --git a/dwds/CONTRIBUTING.md b/dwds/CONTRIBUTING.md
index 4ffa555..ae3abaa 100644
--- a/dwds/CONTRIBUTING.md
+++ b/dwds/CONTRIBUTING.md
@@ -19,8 +19,8 @@
 > - `/PATH_TO_YOUR_FLUTTER_REPO/packages/flutter_tools/bin/flutter_tools.dart`:
 >   This is the path to Flutter Tools itself
 >
-> *More details can be found at the Flutter Tools
-> [README](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/README.md).*
+> _More details can be found at the Flutter Tools
+> [README](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/README.md)._
 
 3. In your Flutter Tools
    [`pubspec.yaml`](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/pubspec.yaml),
@@ -73,8 +73,8 @@
 
 ### Step 1: Roll DWDS into g3
 
-> *NOTE: You must be a Googler to do this step. If you are not, please ask
-> someone for help.*
+> _NOTE: You must be a Googler to do this step. If you are not, please ask
+> someone for help._
 
 - See directions at: go/roll-dwds
 - Wait a few days after rolling into g3 before continuing to step 2. We do so to
@@ -91,21 +91,21 @@
 - From `/dwds` run `dart run build_runner build`, this will build and update the
   version in `/dwds/lib/src/version.dart`
 - Submit a PR with those changes (example PR:
-  https://github.com/dart-lang/webdev/pull/1456). *Note: Ensure your PR doesn’t
-  have any dependency overrides.*
+  https://github.com/dart-lang/webdev/pull/1456). _Note: Ensure your PR doesn’t
+  have any dependency overrides._
 - Once the PR is submitted, pull from master and `run dart pub publish`
 - Finally, go to https://github.com/dart-lang/webdev/releases and create a new
   release, eg https://github.com/dart-lang/webdev/releases/tag/dwds-v12.0.0. You
   might need to delete some of the content of the autogenerated notes.
 
-> *Note: To have the right permissions for publishing, you need to be invited to
+> _Note: To have the right permissions for publishing, you need to be invited to
 > the tools.dart.dev. A member of the Dart team should be able to add you at
-> https://pub.dev/publishers/tools.dart.dev/admin.*
+> https://pub.dev/publishers/tools.dart.dev/admin._
 
 ## Step 3: Publish Webdev to pub
 
-> *Note: DWDS is a dependency of Webdev, which is why DWDS must be published
-> before Webdev can be published.*
+> _Note: DWDS is a dependency of Webdev, which is why DWDS must be published
+> before Webdev can be published._
 
 Follow instructions in the `webdev/webdev`
 [CONTRIBUTING](/webdev/CONTRIBUTING.md) to release Webdev.
@@ -138,3 +138,58 @@
 > run tests for all earlier stable releases of the SDK that match the
 > constraint, which would have differences in functionality and therefore need
 > different tests.
+
+## Hotfixes
+
+Sometimes you might need to do a hotfix release of DWDS. An example of why this
+might be necessary is if you need to do a hotfix of DWDS into Flutter, but don't
+want to release a new version of DWDS with the current untested changes on the
+master branch. Instead you only want to apply a fix to the current version of
+DWDS in Flutter.
+
+### Instructions:
+
+1. Create a branch off the release that needs a hotfix:
+
+   a. In the Github UI's
+   [commit history view](https://github.com/dart-lang/webdev/commits/master),
+   find the commit that prepared the release of DWDS that you would like to
+   hotfix.
+
+   b. Click on `< >` ("Browse the repository at this point in history").
+
+   c. At the top-left, you should see the commit hash in a dropdown. Click the
+   dropdown, and type in a name for your hotfix branch (e.g.
+   `16.0.2-hotfix-release`). Then select "Create branch `16.0.2-hotfix-release`
+   from `commit_hash`".
+
+   d. From your local clone of DWDS, run `git fetch upstream`. (_Note: this
+   assumes you have already configured git to sync your fork with the `upstream`
+   repository. If you haven't, follow
+   [these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo#configuring-git-to-sync-your-fork-with-the-upstream-repository).)_
+
+   e. Search for the branch that you just created, e.g.
+   `git branch -a | grep 16.0.2-hotfix-release` f. Track that branch with
+   `git checkout --track branch_name` (e.g.
+   `remotes/upstream/16.0.2-hotfix-release`)
+
+1. Update the CI tests so that the branch tests against the appropriate Dart
+   SDKs:
+
+   a. Make the appropriate changes to DWDS' `mono_pkg.yaml` then run
+   `mono_repo generate`. Submit this change to the branch you created in step
+   #3, **not** `master`.
+
+1. Make the fix:
+
+   a. You can now make the change you would like to hotfix. From the Github UI,
+   open a PR to merge your change into the branch you created in step #3,
+   **not** `master`. See https://github.com/dart-lang/webdev/pull/1867 as an
+   example.
+
+1. Once it's merged, you can follow the instructions to
+   [publish DWDS to pub](#step-2-publish-dwds-to-pub), except instead of pulling
+   from `master`, pull from the branch your created in step #3.
+
+1. If necessary, open a cherry-pick request in Flutter to update the version.
+   See https://github.com/flutter/flutter/issues/118122 for an example.
diff --git a/dwds/analysis_options.yaml b/dwds/analysis_options.yaml
index 84b1173..3106b5c 100644
--- a/dwds/analysis_options.yaml
+++ b/dwds/analysis_options.yaml
@@ -5,9 +5,15 @@
 
 analyzer:
   exclude:
-      # Ignore generated files
+    # Ignore generated files
     - "lib/data/*"
+    # Ignore debug extension builds
+    - "debug_extension/dev_build/*"
+    - "debug_extension/prod_build/*"
+    - "debug_extension_mv3/dev_build/*"
+    - "debug_extension_mv3/prod_build/*"
 
 linter:
   rules:
     - prefer_final_locals
+    - unawaited_futures
diff --git a/dwds/debug_extension/pubspec.yaml b/dwds/debug_extension/pubspec.yaml
index 8bbc7e4..eec4a91 100644
--- a/dwds/debug_extension/pubspec.yaml
+++ b/dwds/debug_extension/pubspec.yaml
@@ -6,7 +6,7 @@
   A chrome extension for Dart debugging.
 
 environment:
-  sdk: '>=2.12.0 <3.0.0'
+  sdk: ">=3.0.0-134.0.dev <4.0.0"
 
 dependencies:
   async: ^2.3.0
@@ -18,8 +18,8 @@
 
 dev_dependencies:
   build: ^2.0.0
-  build_web_compilers: ^3.0.0
-  build_runner: ^2.0.6
+  build_web_compilers: ^4.0.0
+  build_runner: ^2.4.0
   built_collection: ^5.0.0
   dwds: ^11.0.0
   webdev: ^2.0.0
diff --git a/dwds/debug_extension/tool/update_dev_files.dart b/dwds/debug_extension/tool/update_dev_files.dart
index 24ec970..ece7413 100644
--- a/dwds/debug_extension/tool/update_dev_files.dart
+++ b/dwds/debug_extension/tool/update_dev_files.dart
@@ -2,11 +2,11 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:io';
 
 void main() async {
-  _updateManifestJson();
-  _updateDevtoolsJs();
+  await Future.wait([_updateManifestJson(), _updateDevtoolsJs()]);
 }
 
 /// Adds the Googler extension key, updates the extension icon, and prefixes the
@@ -17,7 +17,7 @@
   final extensionKey = await extensionKeyTxt.exists()
       ? await extensionKeyTxt.readAsString()
       : null;
-  _transformDevFile(manifestJson, (line) {
+  return _transformDevFile(manifestJson, (line) {
     if (_matchesKey(line: line, key: 'name')) {
       return [
         _newKeyValue(
diff --git a/dwds/debug_extension/web/background.dart b/dwds/debug_extension/web/background.dart
index 350291c..08cb079 100644
--- a/dwds/debug_extension/web/background.dart
+++ b/dwds/debug_extension/web/background.dart
@@ -23,6 +23,7 @@
 // NOTE(annagrin): using 'package:dwds/src/utilities/batched_stream.dart'
 // makes dart2js skip creating background.js, so we use a copy instead.
 // import 'package:dwds/src/utilities/batched_stream.dart';
+// Issue: https://github.com/dart-lang/sdk/issues/49973
 import 'package:dwds/src/web_utilities/batched_stream.dart';
 import 'package:js/js.dart';
 import 'package:js/js_util.dart' as js_util;
diff --git a/dwds/debug_extension_mv3/CONTRIBUTING.md b/dwds/debug_extension_mv3/CONTRIBUTING.md
new file mode 100644
index 0000000..18a6049
--- /dev/null
+++ b/dwds/debug_extension_mv3/CONTRIBUTING.md
@@ -0,0 +1,94 @@
+## Building
+
+> Note: First make the script executable: `chmod +x tool/build_extension.sh`
+
+- For development: `./tool/build_extension.sh`
+- For release: `./tool/build_extension.sh prod`
+
+The dart2js-compiled extension will be located in the `/compiled` directory.
+
+## Local Development
+
+### \[For Googlers\] Create an `extension_key.txt` file:
+
+- Create a `extension_key.txt` file at the root of `/debug_extension`. Paste in
+  the value of one of the whitelisted developer keys into this txt file.
+  IMPORTANT: DO NOT COMMIT THE KEY. It will be copied into the `manifest.json`
+  when you build the extension.
+
+### Build and upload your local extension
+
+- Build the extension following the instructions above
+- Visit chrome://extensions
+- Toggle "Developer mode" on
+- Click the "Load unpacked" button
+- Select the extension directory: `/compiled`
+
+### Debug your local extension
+
+- Click the Extensions puzzle piece, and pin the Dart Debug Extension with the
+  dev icon (unpin the published version so you don't confuse them)
+- You can now use the extension normally by clicking it when a local Dart web
+  application has loaded in a Chrome tab
+- To debug, visit chrome://extensions and click "Inspect view on background
+  page" to open Chrome DevTools for the extension
+- More debugging information can be found in the
+  [Chrome Developers documentation](https://developer.chrome.com/docs/extensions/mv3/devguide/)
+
+## Release process
+
+- Update the version in `web/manifest.json`, `pubspec.yaml`, and in the
+  `CHANGELOG`.
+- Follow the instructions above to build the release version of the extension.
+
+> \*At this point, you should manually verify that everything is working by
+> following the steps in [Local Development](#local-development), except load
+> the extension from the `compiled` directory. You will need to add an extension
+> key to the `manifest.json` file in `compiled` to test locally.
+
+- Open a PR to submit the version change.
+- Once submitted, pull the changes down to your local branch, and create a zip
+  of the `compiled` directory. **Remove the Googler extension key that was added
+  by the builder to the `manifest.json` file.**
+- Rename the zip `version_XX.XX.XX.zip` (eg, `version_1.24.0.zip`) and add it to
+  the go/dart-debug-extension-zips folder
+
+> *You must be a Googler to do this. Ask for help if not.*
+
+- Go to the
+  [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole).
+- At the top-right, under Publisher, select dart-bat.
+
+> *If you don’t see dart-bat as an option, you will need someone on the Dart
+> team to add you to the dart-bat Google group.*
+
+- Under Items, select the "Dart Debug Extension".
+- Go to “Package” then select “Upload new package”.
+
+> *The first time you do this, you will be asked to pay a $5 registration fee.
+> The registration fee can be expensed.*
+
+- Upload the zip file you created in step 4.
+- Save as draft, and verify that the new version is correct.
+- Publish. The extension will be published immediately after going through the
+  review process.
+
+## Rollback process
+
+> The Chrome Web Store Developer Dashboard does not support rollbacks. Instead
+> you must re-publish an earlier version. This means that the extension will
+> still have to go through the review process, which can take anywhere from a
+> few hours (most common) to a few days.
+
+- Find the previous version you want to rollback to in the
+  go/dart-debug-extension-zips folder.
+
+> *You must be a Googler to do this. Ask for help if not.*
+
+- Unzip the version you have chosen, and in `manifest.json` edit the version
+  number to be the next sequential version after the current "bad" version (eg,
+  the bad version is `1.28.0` and you are rolling back to version `1.27.0`.
+  Therefore you change `1.27.0` to `1.29.0`).
+- Re-zip the directory and rename it to the new version number. Add it to the
+  go/dart-debug-extension-zips folder.
+- Now, follow steps 6 - 11 in [Release process](#release-process).
diff --git a/dwds/debug_extension_mv3/build.yaml b/dwds/debug_extension_mv3/build.yaml
index b5aaf6d..a1cdac3 100644
--- a/dwds/debug_extension_mv3/build.yaml
+++ b/dwds/debug_extension_mv3/build.yaml
@@ -19,8 +19,9 @@
     build_extensions:
       {
         "web/{{}}.dart.js": ["compiled/{{}}.dart.js"],
-        "web/{{}}.png": ["compiled/{{}}.png"],
-        "web/{{}}.html": ["compiled/{{}}.html"],
+        "web/static_assets/{{}}.png": ["compiled/static_assets/{{}}.png"],
+        "web/static_assets/{{}}.html": ["compiled/static_assets/{{}}.html"],
+        "web/static_assets/{{}}.css": ["compiled/static_assets/{{}}.css"],
         "web/manifest.json": ["compiled/manifest.json"],
       }
     auto_apply: none
diff --git a/dwds/debug_extension_mv3/pubspec.yaml b/dwds/debug_extension_mv3/pubspec.yaml
index a1167d1..e043196 100644
--- a/dwds/debug_extension_mv3/pubspec.yaml
+++ b/dwds/debug_extension_mv3/pubspec.yaml
@@ -6,12 +6,23 @@
   A Chrome extension for Dart debugging.
 
 environment:
-  sdk: '>=2.18.0 <3.0.0'
+  sdk: ">=3.0.0-134.0.dev <4.0.0"
 
 dependencies:
+  built_value: ^8.3.0
+  collection: ^1.15.0
   js: ^0.6.1+1
 
 dev_dependencies:
   build: ^2.0.0
-  build_web_compilers: ^3.0.0
-  build_runner: ^2.0.6
+  build_runner: ^2.4.0
+  built_collection: ^5.0.0
+  built_value_generator: ^8.3.0
+  build_web_compilers: ^4.0.0
+  dwds: ^16.0.0
+  sse: ^4.1.2
+  web_socket_channel: ^2.2.0
+
+dependency_overrides:
+  dwds:
+    path: ..
diff --git a/dwds/debug_extension_mv3/tool/build_extension.sh b/dwds/debug_extension_mv3/tool/build_extension.sh
index 4600bb4..9df8f4f 100755
--- a/dwds/debug_extension_mv3/tool/build_extension.sh
+++ b/dwds/debug_extension_mv3/tool/build_extension.sh
@@ -9,11 +9,24 @@
 # Builds the unminifed dart2js app (see DDC issue: https://github.com/dart-lang/sdk/issues/49869):
 # ./tool/build_extension.sh
 
+
+prod="false"
+
+case "$1" in
+    prod)
+        prod="true"
+        shift;;
+esac
+
 echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
 echo "Building dart2js-compiled extension to /compiled directory."
 echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
 dart run build_runner build web --output build --release
 
+if [ $prod == true ]; then
+    exit 1
+fi
+
 echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
 echo "Updating manifest.json in /compiled directory."
 echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
diff --git a/dwds/debug_extension_mv3/tool/copy_builder.dart b/dwds/debug_extension_mv3/tool/copy_builder.dart
index 5f0760f..93875c9 100644
--- a/dwds/debug_extension_mv3/tool/copy_builder.dart
+++ b/dwds/debug_extension_mv3/tool/copy_builder.dart
@@ -11,8 +11,9 @@
   @override
   Map<String, List<String>> get buildExtensions => {
         "web/{{}}.dart.js": ["compiled/{{}}.dart.js"],
-        "web/{{}}.png": ["compiled/{{}}.png"],
-        "web/{{}}.html": ["compiled/{{}}.html"],
+        "web/static_assets/{{}}.png": ["compiled/static_assets/{{}}.png"],
+        "web/static_assets/{{}}.html": ["compiled/static_assets/{{}}.html"],
+        "web/static_assets/{{}}.css": ["compiled/static_assets/{{}}.css"],
         "web/manifest.json": ["compiled/manifest.json"],
       };
 
diff --git a/dwds/debug_extension_mv3/tool/update_dev_files.dart b/dwds/debug_extension_mv3/tool/update_dev_files.dart
index 22bb4c4..7cc2f48 100644
--- a/dwds/debug_extension_mv3/tool/update_dev_files.dart
+++ b/dwds/debug_extension_mv3/tool/update_dev_files.dart
@@ -2,23 +2,28 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:io';
 
 void main() async {
-  _updateManifestJson();
+  await _updateManifestJson();
 }
 
-/// Adds the Googler extension key.
+/// Adds the Googler extension key, and prefixes the extension name with "DEV".
 Future<void> _updateManifestJson() async {
   final manifestJson = File('compiled/manifest.json');
   final extensionKeyTxt = File('extension_key.txt');
   final extensionKey = await extensionKeyTxt.exists()
       ? await extensionKeyTxt.readAsString()
       : null;
-  _transformDevFile(manifestJson, (line) {
+  return _transformDevFile(manifestJson, (line) {
     if (_matchesKey(line: line, key: 'name')) {
       return [
-        line,
+        _newKeyValue(
+          oldLine: line,
+          newKey: 'name',
+          newValue: '[DEV] MV3 Dart Debug Extension',
+        ),
         if (extensionKey != null)
           _newKeyValue(
             oldLine: line,
diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart
index a971fb4..10436c6 100644
--- a/dwds/debug_extension_mv3/web/background.dart
+++ b/dwds/debug_extension_mv3/web/background.dart
@@ -5,30 +5,210 @@
 @JS()
 library background;
 
+import 'dart:async';
+import 'dart:html';
+
+import 'package:dwds/data/debug_info.dart';
+import 'package:dwds/data/extension_request.dart';
 import 'package:js/js.dart';
 
+import 'data_types.dart';
+import 'debug_session.dart';
 import 'chrome_api.dart';
+import 'cross_extension_communication.dart';
+import 'lifeline_ports.dart';
+import 'logger.dart';
 import 'messaging.dart';
+import 'storage.dart';
+import 'utils.dart';
+import 'web_api.dart';
+
+const _authSuccessResponse = 'Dart Debug Authentication Success!';
 
 void main() {
   _registerListeners();
 }
 
 void _registerListeners() {
-  chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
+  chrome.runtime.onMessage.addListener(
+    allowInterop(_handleRuntimeMessages),
+  );
+  // The only extension allowed to send messages to this extension is the
+  // AngularDart DevTools extension. Its permission is set in the manifest.json
+  // externally_connectable field.
+  chrome.runtime.onMessageExternal.addListener(
+    allowInterop(handleMessagesFromAngularDartDevTools),
+  );
+  chrome.tabs.onRemoved
+      .addListener(allowInterop((tabId, _) => maybeRemoveLifelinePort(tabId)));
+  // Update the extension icon on tab navigation:
+  chrome.tabs.onActivated.addListener(allowInterop((ActiveInfo info) {
+    _updateIcon(info.tabId);
+  }));
+  chrome.windows.onFocusChanged.addListener(allowInterop((_) async {
+    final currentTab = await _getTab();
+    if (currentTab?.id != null) {
+      _updateIcon(currentTab!.id);
+    }
+  }));
+  chrome.webNavigation.onCommitted
+      .addListener(allowInterop(_detectNavigationAwayFromDartApp));
+
+  // Detect clicks on the Dart Debug Extension icon.
+  chrome.action.onClicked.addListener(allowInterop(
+    (Tab tab) => _startDebugSession(
+      tab.id,
+      trigger: Trigger.extensionIcon,
+    ),
+  ));
+}
+
+Future<void> _startDebugSession(int tabId, {required Trigger trigger}) async {
+  final debugInfo = await _fetchDebugInfo(tabId);
+  final extensionUrl = debugInfo?.extensionUrl;
+  if (extensionUrl == null) {
+    _showWarningNotification('Can\'t debug Dart app. Extension URL not found.');
+    sendConnectFailureMessage(
+      ConnectFailureReason.noDartApp,
+      dartAppTabId: tabId,
+    );
+    return;
+  }
+  final isAuthenticated = await _authenticateUser(extensionUrl, tabId);
+  if (!isAuthenticated) {
+    sendConnectFailureMessage(
+      ConnectFailureReason.authentication,
+      dartAppTabId: tabId,
+    );
+    return;
+  }
+
+  maybeCreateLifelinePort(tabId);
+  attachDebugger(tabId, trigger: trigger);
+}
+
+Future<bool> _authenticateUser(String extensionUrl, int tabId) async {
+  final authUrl = _constructAuthUrl(extensionUrl).toString();
+  final response = await fetchRequest(authUrl);
+  final responseBody = response.body ?? '';
+  if (!responseBody.contains(_authSuccessResponse)) {
+    debugError('Not authenticated: ${response.status} / $responseBody',
+        verbose: true);
+    _showWarningNotification('Please re-authenticate and try again.');
+    await createTab(authUrl, inNewWindow: false);
+    return false;
+  }
+  return true;
+}
+
+Uri _constructAuthUrl(String extensionUrl) {
+  final authUri = Uri.parse(extensionUrl).replace(path: authenticationPath);
+  if (authUri.scheme == 'ws') {
+    return authUri.replace(scheme: 'http');
+  }
+  if (authUri.scheme == 'wss') {
+    return authUri.replace(scheme: 'https');
+  }
+  return authUri;
 }
 
 void _handleRuntimeMessages(
     dynamic jsRequest, MessageSender sender, Function sendResponse) async {
   if (jsRequest is! String) return;
 
-  interceptMessage(
+  interceptMessage<DebugInfo>(
       message: jsRequest,
+      expectedType: MessageType.debugInfo,
       expectedSender: Script.detector,
       expectedRecipient: Script.background,
-      expectedType: MessageType.dartAppReady,
-      messageHandler: (_) {
+      messageHandler: (DebugInfo debugInfo) async {
+        final dartTab = sender.tab;
+        if (dartTab == null) {
+          debugWarn('Received debug info but tab is missing.');
+          return;
+        }
+        // Save the debug info for the Dart app in storage:
+        await setStorageObject<DebugInfo>(
+            type: StorageObject.debugInfo, value: debugInfo, tabId: dartTab.id);
         // Update the icon to show that a Dart app has been detected:
-        chrome.action.setIcon(IconInfo(path: 'dart.png'), /*callback*/ null);
+        final currentTab = await _getTab();
+        if (currentTab?.id == dartTab.id) {
+          _setDebuggableIcon();
+        }
       });
+
+  interceptMessage<DebugStateChange>(
+      message: jsRequest,
+      expectedType: MessageType.debugStateChange,
+      expectedSender: Script.debuggerPanel,
+      expectedRecipient: Script.background,
+      messageHandler: (DebugStateChange debugStateChange) {
+        final newState = debugStateChange.newState;
+        final tabId = debugStateChange.tabId;
+        if (newState == DebugStateChange.startDebugging) {
+          _startDebugSession(tabId, trigger: Trigger.extensionPanel);
+        }
+      });
+}
+
+void _detectNavigationAwayFromDartApp(NavigationInfo navigationInfo) async {
+  final tabId = navigationInfo.tabId;
+  final debugInfo = await _fetchDebugInfo(navigationInfo.tabId);
+  if (debugInfo == null) return;
+  if (debugInfo.appUrl != navigationInfo.url) {
+    _setDefaultIcon();
+    await removeStorageObject(type: StorageObject.debugInfo, tabId: tabId);
+    detachDebugger(
+      tabId,
+      type: TabType.dartApp,
+      reason: DetachReason.navigatedAwayFromApp,
+    );
+  }
+}
+
+void _updateIcon(int activeTabId) async {
+  final debugInfo = await _fetchDebugInfo(activeTabId);
+  if (debugInfo != null) {
+    _setDebuggableIcon();
+  } else {
+    _setDefaultIcon();
+  }
+}
+
+void _setDebuggableIcon() {
+  chrome.action
+      .setIcon(IconInfo(path: 'static_assets/dart.png'), /*callback*/ null);
+}
+
+void _setDefaultIcon() {
+  final iconPath = isDevMode()
+      ? 'static_assets/dart_dev.png'
+      : 'static_assets/dart_grey.png';
+  chrome.action.setIcon(IconInfo(path: iconPath), /*callback*/ null);
+}
+
+Future<DebugInfo?> _fetchDebugInfo(int tabId) {
+  return fetchStorageObject<DebugInfo>(
+    type: StorageObject.debugInfo,
+    tabId: tabId,
+  );
+}
+
+void _showWarningNotification(String message) {
+  chrome.notifications.create(
+    /*notificationId*/ null,
+    NotificationOptions(
+      title: '[Error] Dart Debug Extension',
+      message: message,
+      iconUrl: 'static_assets/dart.png',
+      type: 'basic',
+    ),
+    /*callback*/ null,
+  );
+}
+
+Future<Tab?> _getTab() async {
+  final query = QueryInfo(active: true, currentWindow: true);
+  final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
+  return tabs.isNotEmpty ? tabs.first : null;
 }
diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart
index 82f33dd..77fc960 100644
--- a/dwds/debug_extension_mv3/web/chrome_api.dart
+++ b/dwds/debug_extension_mv3/web/chrome_api.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:html';
+
 import 'package:js/js.dart';
 
 @JS()
@@ -11,7 +13,15 @@
 @anonymous
 class Chrome {
   external Action get action;
+  external Debugger get debugger;
+  external Devtools get devtools;
+  external Notifications get notifications;
   external Runtime get runtime;
+  external Scripting get scripting;
+  external Storage get storage;
+  external Tabs get tabs;
+  external WebNavigation get webNavigation;
+  external Windows get windows;
 }
 
 /// chrome.action APIs
@@ -38,16 +48,164 @@
   external factory IconInfo({String path});
 }
 
+/// chrome.debugger APIs:
+/// https://developer.chrome.com/docs/extensions/reference/debugger
+
+@JS()
+@anonymous
+class Debugger {
+  external void attach(
+      Debuggee target, String requiredVersion, Function? callback);
+
+  external Object detach(Debuggee target);
+
+  external void sendCommand(Debuggee target, String method,
+      Object? commandParams, Function? callback);
+
+  external OnDetachHandler get onDetach;
+
+  external OnEventHandler get onEvent;
+}
+
+@JS()
+@anonymous
+class OnDetachHandler {
+  external void addListener(
+      void Function(Debuggee source, String reason) callback);
+}
+
+@JS()
+@anonymous
+class OnEventHandler {
+  external void addListener(
+      void Function(Debuggee source, String method, Object? params) callback);
+}
+
+@JS()
+@anonymous
+class Debuggee {
+  external int get tabId;
+  external String get extensionId;
+  external String get targetId;
+  external factory Debuggee({int tabId, String? extensionId, String? targetId});
+}
+
+/// chrome.devtools APIs:
+
+@JS()
+@anonymous
+class Devtools {
+  // https://developer.chrome.com/docs/extensions/reference/devtools_inspectedWindow
+  external InspectedWindow get inspectedWindow;
+
+  // https://developer.chrome.com/docs/extensions/reference/devtools_panels/
+  external Panels get panels;
+}
+
+@JS()
+@anonymous
+class InspectedWindow {
+  external int get tabId;
+}
+
+@JS()
+@anonymous
+class Panels {
+  external String get themeName;
+
+  external void create(String title, String iconPath, String pagePath,
+      void Function(ExtensionPanel)? callback);
+}
+
+@JS()
+@anonymous
+class ExtensionPanel {
+  external OnHiddenHandler get onHidden;
+  external OnShownHandler get onShown;
+}
+
+@JS()
+@anonymous
+class OnHiddenHandler {
+  external void addListener(void Function() callback);
+}
+
+@JS()
+@anonymous
+class OnShownHandler {
+  external void addListener(void Function(Window window) callback);
+}
+
+/// chrome.notification APIs:
+/// https://developer.chrome.com/docs/extensions/reference/notifications
+
+@JS()
+@anonymous
+class Notifications {
+  external void create(
+      String? notificationId, NotificationOptions options, Function? callback);
+}
+
+@JS()
+@anonymous
+class NotificationOptions {
+  external factory NotificationOptions({
+    String title,
+    String message,
+    String iconUrl,
+    String type,
+  });
+}
+
 /// chrome.runtime APIs:
 /// https://developer.chrome.com/docs/extensions/reference/runtime
 
 @JS()
 @anonymous
 class Runtime {
+  external void connect(String? extensionId, ConnectInfo info);
+
   external void sendMessage(
       String? id, Object? message, Object? options, Function? callback);
 
+  external Object getManifest();
+
+  external String getURL(String path);
+
+  // Note: Not checking the lastError when one occurs throws a runtime exception.
+  external ChromeError? get lastError;
+
+  external ConnectionHandler get onConnect;
+
   external OnMessageHandler get onMessage;
+
+  external OnMessageHandler get onMessageExternal;
+}
+
+@JS()
+class ChromeError {
+  external String get message;
+}
+
+@JS()
+@anonymous
+class ConnectInfo {
+  external String? get name;
+  external factory ConnectInfo({String? name});
+}
+
+@JS()
+@anonymous
+class Port {
+  external String? get name;
+  external void disconnect();
+  external ConnectionHandler get onDisconnect;
+}
+
+@JS()
+@anonymous
+class ConnectionHandler {
+  external void addListener(void Function(Port) callback);
 }
 
 @JS()
@@ -66,9 +224,182 @@
   external factory MessageSender({String? id, String? url, Tab? tab});
 }
 
+/// chrome.scripting APIs
+/// https://developer.chrome.com/docs/extensions/reference/scripting
+
+@JS()
+@anonymous
+class Scripting {
+  external executeScript(InjectDetails details, Function? callback);
+}
+
+@JS()
+@anonymous
+class InjectDetails<T, U> {
+  external Target get target;
+  external T? get func;
+  external List<U?>? get args;
+  external List<String>? get files;
+  external factory InjectDetails({
+    Target target,
+    T? func,
+    List<U>? args,
+    List<String>? files,
+  });
+}
+
+@JS()
+@anonymous
+class Target {
+  external int get tabId;
+  external factory Target({int tabId});
+}
+
+/// chrome.storage APIs
+/// https://developer.chrome.com/docs/extensions/reference/storage
+
+@JS()
+@anonymous
+class Storage {
+  external StorageArea get local;
+
+  external StorageArea get session;
+
+  external OnChangedHandler get onChanged;
+}
+
+@JS()
+@anonymous
+class StorageArea {
+  external Object get(List<String> keys, void Function(Object result) callback);
+
+  external Object set(Object items, void Function()? callback);
+
+  external Object remove(List<String> keys, void Function()? callback);
+}
+
+@JS()
+@anonymous
+class OnChangedHandler {
+  external void addListener(
+    void Function(Object changes, String areaName) callback,
+  );
+}
+
+/// chrome.tabs APIs
+/// https://developer.chrome.com/docs/extensions/reference/tabs
+
+@JS()
+@anonymous
+class Tabs {
+  external Object query(QueryInfo queryInfo);
+
+  external Object create(TabInfo tabInfo);
+
+  external Object get(int tabId);
+
+  external Object remove(int tabId);
+
+  external OnActivatedHandler get onActivated;
+
+  external OnRemovedHandler get onRemoved;
+}
+
+@JS()
+@anonymous
+class OnActivatedHandler {
+  external void addListener(void Function(ActiveInfo activeInfo) callback);
+}
+
+@JS()
+@anonymous
+class OnRemovedHandler {
+  external void addListener(void Function(int tabId, dynamic info) callback);
+}
+
+@JS()
+@anonymous
+class ActiveInfo {
+  external int get tabId;
+}
+
+@JS()
+@anonymous
+class TabInfo {
+  external bool? get active;
+  external bool? get pinned;
+  external String? get url;
+  external factory TabInfo({bool? active, bool? pinned, String? url});
+}
+
+@JS()
+@anonymous
+class QueryInfo {
+  external bool get active;
+  external bool get currentWindow;
+  external String get url;
+  external factory QueryInfo({bool? active, bool? currentWindow, String? url});
+}
+
 @JS()
 @anonymous
 class Tab {
   external int get id;
   external String get url;
 }
+
+/// chrome.webNavigation APIs
+/// https://developer.chrome.com/docs/extensions/reference/webNavigation
+
+@JS()
+@anonymous
+class WebNavigation {
+  // https://developer.chrome.com/docs/extensions/reference/webNavigation/#event-onCommitted
+  external OnCommittedHandler get onCommitted;
+}
+
+@JS()
+@anonymous
+class OnCommittedHandler {
+  external void addListener(void Function(NavigationInfo details) callback);
+}
+
+@JS()
+@anonymous
+class NavigationInfo {
+  external String get transitionType;
+  external int get tabId;
+  external String get url;
+}
+
+/// chrome.windows APIs
+/// https://developer.chrome.com/docs/extensions/reference/windows
+
+@JS()
+@anonymous
+class Windows {
+  external Object create(WindowInfo? createData);
+
+  external OnFocusChangedHandler get onFocusChanged;
+}
+
+@JS()
+@anonymous
+class OnFocusChangedHandler {
+  external void addListener(void Function(int windowId) callback);
+}
+
+@JS()
+@anonymous
+class WindowInfo {
+  external bool? get focused;
+  external String? get url;
+  external factory WindowInfo({bool? focused, String? url});
+}
+
+@JS()
+@anonymous
+class WindowObj {
+  external int get id;
+  external List<Tab> get tabs;
+}
diff --git a/dwds/debug_extension_mv3/web/cross_extension_communication.dart b/dwds/debug_extension_mv3/web/cross_extension_communication.dart
new file mode 100644
index 0000000..77faf5e
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/cross_extension_communication.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library cross_extension_communication;
+
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'debug_session.dart';
+import 'logger.dart';
+import 'storage.dart';
+import 'web_api.dart';
+
+// The only extension allowed to communicate with this extension is the
+// AngularDart DevTools extension.
+//
+// This ID is used to send messages to AngularDart DevTools, while the
+// externally_connectable field in the manifest.json allows AngularDart DevTools
+// to send messages to this extension.
+const _angularDartDevToolsId = 'nbkbficgbembimioedhceniahniffgpl';
+
+// A set of events to forward to the AngularDart DevTools extension.
+final _eventsForAngularDartDevTools = {
+  'Overlay.inspectNodeRequested',
+  'dwds.encodedUri',
+};
+
+void handleMessagesFromAngularDartDevTools(
+    dynamic jsRequest, MessageSender sender, Function sendResponse) async {
+  if (jsRequest == null) return;
+  final message = jsRequest as ExternalExtensionMessage;
+  if (message.name == 'chrome.debugger.sendCommand') {
+    _forwardCommandToChromeDebugger(message, sendResponse);
+  } else if (message.name == 'dwds.encodedUri') {
+    _respondWithEncodedUri(message.tabId, sendResponse);
+  } else if (message.name == 'dwds.startDebugging') {
+    attachDebugger(message.tabId, trigger: Trigger.angularDartDevTools);
+    sendResponse(true);
+  } else {
+    sendResponse(
+        ErrorResponse()..error = 'Unknown message name: ${message.name}');
+  }
+}
+
+void maybeForwardMessageToAngularDartDevTools(
+    {required String method, required dynamic params, required int tabId}) {
+  if (!_eventsForAngularDartDevTools.contains(method)) return;
+
+  final message = method.startsWith('dwds')
+      ? _dwdsEventMessage(method: method, params: params, tabId: tabId)
+      : _debugEventMessage(method: method, params: params, tabId: tabId);
+
+  _forwardMessageToAngularDartDevTools(message);
+}
+
+void _forwardCommandToChromeDebugger(
+    ExternalExtensionMessage message, Function sendResponse) {
+  try {
+    final options = message.options as SendCommandOptions;
+    chrome.debugger.sendCommand(
+      Debuggee(tabId: message.tabId),
+      options.method,
+      options.commandParams,
+      allowInterop(
+          ([result]) => _respondWithChromeResult(result, sendResponse)),
+    );
+  } catch (e) {
+    sendResponse(ErrorResponse()..error = '$e');
+  }
+}
+
+void _respondWithChromeResult(Object? chromeResult, Function sendResponse) {
+  // No result indicates that an error occurred.
+  if (chromeResult == null) {
+    sendResponse(ErrorResponse()
+      ..error = JSON.stringify(
+        chrome.runtime.lastError ?? 'Unknown error.',
+      ));
+  } else {
+    sendResponse(chromeResult);
+  }
+}
+
+void _respondWithEncodedUri(int tabId, Function sendResponse) async {
+  final encodedUri = await fetchStorageObject<String>(
+      type: StorageObject.encodedUri, tabId: tabId);
+  sendResponse(encodedUri ?? '');
+}
+
+void _forwardMessageToAngularDartDevTools(ExternalExtensionMessage message) {
+  chrome.runtime.sendMessage(
+    _angularDartDevToolsId,
+    message,
+    /* options */ null,
+    allowInterop(([result]) => _checkForErrors(result, message.name)),
+  );
+}
+
+void _checkForErrors(Object? chromeResult, String messageName) {
+  // No result indicates that an error occurred.
+  if (chromeResult == null) {
+    final errorMessage = chrome.runtime.lastError?.message ?? 'Unknown error.';
+    debugWarn('Error forwarding $messageName: $errorMessage');
+  }
+}
+
+ExternalExtensionMessage _debugEventMessage({
+  required String method,
+  required dynamic params,
+  required int tabId,
+}) =>
+    ExternalExtensionMessage(
+      name: 'chrome.debugger.event',
+      tabId: tabId,
+      options: DebugEvent(method: method, params: params),
+    );
+
+ExternalExtensionMessage _dwdsEventMessage({
+  required String method,
+  required dynamic params,
+  required int tabId,
+}) =>
+    ExternalExtensionMessage(
+      name: method,
+      tabId: tabId,
+      options: params,
+    );
+
+// This message is used for cross-extension communication between this extension
+// and the AngularDart DevTools extension.
+@JS()
+@anonymous
+class ExternalExtensionMessage {
+  external int get tabId;
+  external String get name;
+  external dynamic get options;
+  external factory ExternalExtensionMessage(
+      {required int tabId, required String name, required dynamic options});
+}
+
+@JS()
+@anonymous
+class DebugEvent {
+  external factory DebugEvent({String method, Object? params});
+}
+
+@JS()
+@anonymous
+class SendCommandOptions {
+  external String get method;
+  external Object get commandParams;
+}
+
+@JS()
+@anonymous
+class ErrorResponse {
+  external set error(String error);
+}
diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart
new file mode 100644
index 0000000..9c4e314
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/data_serializers.dart
@@ -0,0 +1,28 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:built_collection/built_collection.dart';
+import 'package:built_value/serializer.dart';
+import 'package:dwds/data/debug_info.dart';
+import 'package:dwds/data/devtools_request.dart';
+import 'package:dwds/data/extension_request.dart';
+
+import 'data_types.dart';
+
+part 'data_serializers.g.dart';
+
+/// Serializers for all the data types used in the Dart Debug Extension.
+@SerializersFor([
+  BatchedEvents,
+  ConnectFailure,
+  DebugInfo,
+  DebugStateChange,
+  DevToolsOpener,
+  DevToolsUrl,
+  DevToolsRequest,
+  ExtensionEvent,
+  ExtensionRequest,
+  ExtensionResponse,
+])
+final Serializers serializers = _$serializers;
diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart
new file mode 100644
index 0000000..7c15ed1
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'data_serializers.dart';
+
+// **************************************************************************
+// BuiltValueGenerator
+// **************************************************************************
+
+Serializers _$serializers = (new Serializers().toBuilder()
+      ..add(BatchedEvents.serializer)
+      ..add(ConnectFailure.serializer)
+      ..add(DebugInfo.serializer)
+      ..add(DebugStateChange.serializer)
+      ..add(DevToolsOpener.serializer)
+      ..add(DevToolsRequest.serializer)
+      ..add(DevToolsUrl.serializer)
+      ..add(ExtensionEvent.serializer)
+      ..add(ExtensionRequest.serializer)
+      ..add(ExtensionResponse.serializer)
+      ..addBuilderFactory(
+          const FullType(BuiltList, const [const FullType(ExtensionEvent)]),
+          () => new ListBuilder<ExtensionEvent>()))
+    .build();
+
+// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas
diff --git a/dwds/debug_extension_mv3/web/data_types.dart b/dwds/debug_extension_mv3/web/data_types.dart
new file mode 100644
index 0000000..eb22eb4
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/data_types.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:built_value/built_value.dart';
+import 'package:built_value/serializer.dart';
+
+part 'data_types.g.dart';
+
+abstract class ConnectFailure
+    implements Built<ConnectFailure, ConnectFailureBuilder> {
+  static Serializer<ConnectFailure> get serializer =>
+      _$connectFailureSerializer;
+
+  factory ConnectFailure([Function(ConnectFailureBuilder) updates]) =
+      _$ConnectFailure;
+
+  ConnectFailure._();
+
+  int get tabId;
+
+  String? get reason;
+}
+
+abstract class DevToolsOpener
+    implements Built<DevToolsOpener, DevToolsOpenerBuilder> {
+  static Serializer<DevToolsOpener> get serializer =>
+      _$devToolsOpenerSerializer;
+
+  factory DevToolsOpener([Function(DevToolsOpenerBuilder) updates]) =
+      _$DevToolsOpener;
+
+  DevToolsOpener._();
+
+  bool get newWindow;
+}
+
+abstract class DevToolsUrl implements Built<DevToolsUrl, DevToolsUrlBuilder> {
+  static Serializer<DevToolsUrl> get serializer => _$devToolsUrlSerializer;
+
+  factory DevToolsUrl([Function(DevToolsUrlBuilder) updates]) = _$DevToolsUrl;
+
+  DevToolsUrl._();
+
+  int get tabId;
+
+  String get url;
+}
+
+abstract class DebugStateChange
+    implements Built<DebugStateChange, DebugStateChangeBuilder> {
+  static const startDebugging = 'start-debugging';
+  static const stopDebugging = 'stop-debugging';
+  static const failedToConnect = 'failed-to-connect';
+
+  static Serializer<DebugStateChange> get serializer =>
+      _$debugStateChangeSerializer;
+
+  factory DebugStateChange([Function(DebugStateChangeBuilder) updates]) =
+      _$DebugStateChange;
+
+  DebugStateChange._();
+
+  int get tabId;
+
+  /// Can only be [startDebugging] or [stopDebugging].
+  String get newState;
+
+  String? get reason;
+}
diff --git a/dwds/debug_extension_mv3/web/data_types.g.dart b/dwds/debug_extension_mv3/web/data_types.g.dart
new file mode 100644
index 0000000..34c78b4
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/data_types.g.dart
@@ -0,0 +1,588 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'data_types.dart';
+
+// **************************************************************************
+// BuiltValueGenerator
+// **************************************************************************
+
+Serializer<ConnectFailure> _$connectFailureSerializer =
+    new _$ConnectFailureSerializer();
+Serializer<DevToolsOpener> _$devToolsOpenerSerializer =
+    new _$DevToolsOpenerSerializer();
+Serializer<DevToolsUrl> _$devToolsUrlSerializer = new _$DevToolsUrlSerializer();
+Serializer<DebugStateChange> _$debugStateChangeSerializer =
+    new _$DebugStateChangeSerializer();
+
+class _$ConnectFailureSerializer
+    implements StructuredSerializer<ConnectFailure> {
+  @override
+  final Iterable<Type> types = const [ConnectFailure, _$ConnectFailure];
+  @override
+  final String wireName = 'ConnectFailure';
+
+  @override
+  Iterable<Object?> serialize(Serializers serializers, ConnectFailure object,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = <Object?>[
+      'tabId',
+      serializers.serialize(object.tabId, specifiedType: const FullType(int)),
+    ];
+    Object? value;
+    value = object.reason;
+    if (value != null) {
+      result
+        ..add('reason')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    return result;
+  }
+
+  @override
+  ConnectFailure deserialize(
+      Serializers serializers, Iterable<Object?> serialized,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = new ConnectFailureBuilder();
+
+    final iterator = serialized.iterator;
+    while (iterator.moveNext()) {
+      final key = iterator.current! as String;
+      iterator.moveNext();
+      final Object? value = iterator.current;
+      switch (key) {
+        case 'tabId':
+          result.tabId = serializers.deserialize(value,
+              specifiedType: const FullType(int))! as int;
+          break;
+        case 'reason':
+          result.reason = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+      }
+    }
+
+    return result.build();
+  }
+}
+
+class _$DevToolsOpenerSerializer
+    implements StructuredSerializer<DevToolsOpener> {
+  @override
+  final Iterable<Type> types = const [DevToolsOpener, _$DevToolsOpener];
+  @override
+  final String wireName = 'DevToolsOpener';
+
+  @override
+  Iterable<Object?> serialize(Serializers serializers, DevToolsOpener object,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = <Object?>[
+      'newWindow',
+      serializers.serialize(object.newWindow,
+          specifiedType: const FullType(bool)),
+    ];
+
+    return result;
+  }
+
+  @override
+  DevToolsOpener deserialize(
+      Serializers serializers, Iterable<Object?> serialized,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = new DevToolsOpenerBuilder();
+
+    final iterator = serialized.iterator;
+    while (iterator.moveNext()) {
+      final key = iterator.current! as String;
+      iterator.moveNext();
+      final Object? value = iterator.current;
+      switch (key) {
+        case 'newWindow':
+          result.newWindow = serializers.deserialize(value,
+              specifiedType: const FullType(bool))! as bool;
+          break;
+      }
+    }
+
+    return result.build();
+  }
+}
+
+class _$DevToolsUrlSerializer implements StructuredSerializer<DevToolsUrl> {
+  @override
+  final Iterable<Type> types = const [DevToolsUrl, _$DevToolsUrl];
+  @override
+  final String wireName = 'DevToolsUrl';
+
+  @override
+  Iterable<Object?> serialize(Serializers serializers, DevToolsUrl object,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = <Object?>[
+      'tabId',
+      serializers.serialize(object.tabId, specifiedType: const FullType(int)),
+      'url',
+      serializers.serialize(object.url, specifiedType: const FullType(String)),
+    ];
+
+    return result;
+  }
+
+  @override
+  DevToolsUrl deserialize(Serializers serializers, Iterable<Object?> serialized,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = new DevToolsUrlBuilder();
+
+    final iterator = serialized.iterator;
+    while (iterator.moveNext()) {
+      final key = iterator.current! as String;
+      iterator.moveNext();
+      final Object? value = iterator.current;
+      switch (key) {
+        case 'tabId':
+          result.tabId = serializers.deserialize(value,
+              specifiedType: const FullType(int))! as int;
+          break;
+        case 'url':
+          result.url = serializers.deserialize(value,
+              specifiedType: const FullType(String))! as String;
+          break;
+      }
+    }
+
+    return result.build();
+  }
+}
+
+class _$DebugStateChangeSerializer
+    implements StructuredSerializer<DebugStateChange> {
+  @override
+  final Iterable<Type> types = const [DebugStateChange, _$DebugStateChange];
+  @override
+  final String wireName = 'DebugStateChange';
+
+  @override
+  Iterable<Object?> serialize(Serializers serializers, DebugStateChange object,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = <Object?>[
+      'tabId',
+      serializers.serialize(object.tabId, specifiedType: const FullType(int)),
+      'newState',
+      serializers.serialize(object.newState,
+          specifiedType: const FullType(String)),
+    ];
+    Object? value;
+    value = object.reason;
+    if (value != null) {
+      result
+        ..add('reason')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    return result;
+  }
+
+  @override
+  DebugStateChange deserialize(
+      Serializers serializers, Iterable<Object?> serialized,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = new DebugStateChangeBuilder();
+
+    final iterator = serialized.iterator;
+    while (iterator.moveNext()) {
+      final key = iterator.current! as String;
+      iterator.moveNext();
+      final Object? value = iterator.current;
+      switch (key) {
+        case 'tabId':
+          result.tabId = serializers.deserialize(value,
+              specifiedType: const FullType(int))! as int;
+          break;
+        case 'newState':
+          result.newState = serializers.deserialize(value,
+              specifiedType: const FullType(String))! as String;
+          break;
+        case 'reason':
+          result.reason = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+      }
+    }
+
+    return result.build();
+  }
+}
+
+class _$ConnectFailure extends ConnectFailure {
+  @override
+  final int tabId;
+  @override
+  final String? reason;
+
+  factory _$ConnectFailure([void Function(ConnectFailureBuilder)? updates]) =>
+      (new ConnectFailureBuilder()..update(updates))._build();
+
+  _$ConnectFailure._({required this.tabId, this.reason}) : super._() {
+    BuiltValueNullFieldError.checkNotNull(tabId, r'ConnectFailure', 'tabId');
+  }
+
+  @override
+  ConnectFailure rebuild(void Function(ConnectFailureBuilder) updates) =>
+      (toBuilder()..update(updates)).build();
+
+  @override
+  ConnectFailureBuilder toBuilder() =>
+      new ConnectFailureBuilder()..replace(this);
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) return true;
+    return other is ConnectFailure &&
+        tabId == other.tabId &&
+        reason == other.reason;
+  }
+
+  @override
+  int get hashCode {
+    return $jf($jc($jc(0, tabId.hashCode), reason.hashCode));
+  }
+
+  @override
+  String toString() {
+    return (newBuiltValueToStringHelper(r'ConnectFailure')
+          ..add('tabId', tabId)
+          ..add('reason', reason))
+        .toString();
+  }
+}
+
+class ConnectFailureBuilder
+    implements Builder<ConnectFailure, ConnectFailureBuilder> {
+  _$ConnectFailure? _$v;
+
+  int? _tabId;
+  int? get tabId => _$this._tabId;
+  set tabId(int? tabId) => _$this._tabId = tabId;
+
+  String? _reason;
+  String? get reason => _$this._reason;
+  set reason(String? reason) => _$this._reason = reason;
+
+  ConnectFailureBuilder();
+
+  ConnectFailureBuilder get _$this {
+    final $v = _$v;
+    if ($v != null) {
+      _tabId = $v.tabId;
+      _reason = $v.reason;
+      _$v = null;
+    }
+    return this;
+  }
+
+  @override
+  void replace(ConnectFailure other) {
+    ArgumentError.checkNotNull(other, 'other');
+    _$v = other as _$ConnectFailure;
+  }
+
+  @override
+  void update(void Function(ConnectFailureBuilder)? updates) {
+    if (updates != null) updates(this);
+  }
+
+  @override
+  ConnectFailure build() => _build();
+
+  _$ConnectFailure _build() {
+    final _$result = _$v ??
+        new _$ConnectFailure._(
+            tabId: BuiltValueNullFieldError.checkNotNull(
+                tabId, r'ConnectFailure', 'tabId'),
+            reason: reason);
+    replace(_$result);
+    return _$result;
+  }
+}
+
+class _$DevToolsOpener extends DevToolsOpener {
+  @override
+  final bool newWindow;
+
+  factory _$DevToolsOpener([void Function(DevToolsOpenerBuilder)? updates]) =>
+      (new DevToolsOpenerBuilder()..update(updates))._build();
+
+  _$DevToolsOpener._({required this.newWindow}) : super._() {
+    BuiltValueNullFieldError.checkNotNull(
+        newWindow, r'DevToolsOpener', 'newWindow');
+  }
+
+  @override
+  DevToolsOpener rebuild(void Function(DevToolsOpenerBuilder) updates) =>
+      (toBuilder()..update(updates)).build();
+
+  @override
+  DevToolsOpenerBuilder toBuilder() =>
+      new DevToolsOpenerBuilder()..replace(this);
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) return true;
+    return other is DevToolsOpener && newWindow == other.newWindow;
+  }
+
+  @override
+  int get hashCode {
+    return $jf($jc(0, newWindow.hashCode));
+  }
+
+  @override
+  String toString() {
+    return (newBuiltValueToStringHelper(r'DevToolsOpener')
+          ..add('newWindow', newWindow))
+        .toString();
+  }
+}
+
+class DevToolsOpenerBuilder
+    implements Builder<DevToolsOpener, DevToolsOpenerBuilder> {
+  _$DevToolsOpener? _$v;
+
+  bool? _newWindow;
+  bool? get newWindow => _$this._newWindow;
+  set newWindow(bool? newWindow) => _$this._newWindow = newWindow;
+
+  DevToolsOpenerBuilder();
+
+  DevToolsOpenerBuilder get _$this {
+    final $v = _$v;
+    if ($v != null) {
+      _newWindow = $v.newWindow;
+      _$v = null;
+    }
+    return this;
+  }
+
+  @override
+  void replace(DevToolsOpener other) {
+    ArgumentError.checkNotNull(other, 'other');
+    _$v = other as _$DevToolsOpener;
+  }
+
+  @override
+  void update(void Function(DevToolsOpenerBuilder)? updates) {
+    if (updates != null) updates(this);
+  }
+
+  @override
+  DevToolsOpener build() => _build();
+
+  _$DevToolsOpener _build() {
+    final _$result = _$v ??
+        new _$DevToolsOpener._(
+            newWindow: BuiltValueNullFieldError.checkNotNull(
+                newWindow, r'DevToolsOpener', 'newWindow'));
+    replace(_$result);
+    return _$result;
+  }
+}
+
+class _$DevToolsUrl extends DevToolsUrl {
+  @override
+  final int tabId;
+  @override
+  final String url;
+
+  factory _$DevToolsUrl([void Function(DevToolsUrlBuilder)? updates]) =>
+      (new DevToolsUrlBuilder()..update(updates))._build();
+
+  _$DevToolsUrl._({required this.tabId, required this.url}) : super._() {
+    BuiltValueNullFieldError.checkNotNull(tabId, r'DevToolsUrl', 'tabId');
+    BuiltValueNullFieldError.checkNotNull(url, r'DevToolsUrl', 'url');
+  }
+
+  @override
+  DevToolsUrl rebuild(void Function(DevToolsUrlBuilder) updates) =>
+      (toBuilder()..update(updates)).build();
+
+  @override
+  DevToolsUrlBuilder toBuilder() => new DevToolsUrlBuilder()..replace(this);
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) return true;
+    return other is DevToolsUrl && tabId == other.tabId && url == other.url;
+  }
+
+  @override
+  int get hashCode {
+    return $jf($jc($jc(0, tabId.hashCode), url.hashCode));
+  }
+
+  @override
+  String toString() {
+    return (newBuiltValueToStringHelper(r'DevToolsUrl')
+          ..add('tabId', tabId)
+          ..add('url', url))
+        .toString();
+  }
+}
+
+class DevToolsUrlBuilder implements Builder<DevToolsUrl, DevToolsUrlBuilder> {
+  _$DevToolsUrl? _$v;
+
+  int? _tabId;
+  int? get tabId => _$this._tabId;
+  set tabId(int? tabId) => _$this._tabId = tabId;
+
+  String? _url;
+  String? get url => _$this._url;
+  set url(String? url) => _$this._url = url;
+
+  DevToolsUrlBuilder();
+
+  DevToolsUrlBuilder get _$this {
+    final $v = _$v;
+    if ($v != null) {
+      _tabId = $v.tabId;
+      _url = $v.url;
+      _$v = null;
+    }
+    return this;
+  }
+
+  @override
+  void replace(DevToolsUrl other) {
+    ArgumentError.checkNotNull(other, 'other');
+    _$v = other as _$DevToolsUrl;
+  }
+
+  @override
+  void update(void Function(DevToolsUrlBuilder)? updates) {
+    if (updates != null) updates(this);
+  }
+
+  @override
+  DevToolsUrl build() => _build();
+
+  _$DevToolsUrl _build() {
+    final _$result = _$v ??
+        new _$DevToolsUrl._(
+            tabId: BuiltValueNullFieldError.checkNotNull(
+                tabId, r'DevToolsUrl', 'tabId'),
+            url: BuiltValueNullFieldError.checkNotNull(
+                url, r'DevToolsUrl', 'url'));
+    replace(_$result);
+    return _$result;
+  }
+}
+
+class _$DebugStateChange extends DebugStateChange {
+  @override
+  final int tabId;
+  @override
+  final String newState;
+  @override
+  final String? reason;
+
+  factory _$DebugStateChange(
+          [void Function(DebugStateChangeBuilder)? updates]) =>
+      (new DebugStateChangeBuilder()..update(updates))._build();
+
+  _$DebugStateChange._(
+      {required this.tabId, required this.newState, this.reason})
+      : super._() {
+    BuiltValueNullFieldError.checkNotNull(tabId, r'DebugStateChange', 'tabId');
+    BuiltValueNullFieldError.checkNotNull(
+        newState, r'DebugStateChange', 'newState');
+  }
+
+  @override
+  DebugStateChange rebuild(void Function(DebugStateChangeBuilder) updates) =>
+      (toBuilder()..update(updates)).build();
+
+  @override
+  DebugStateChangeBuilder toBuilder() =>
+      new DebugStateChangeBuilder()..replace(this);
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) return true;
+    return other is DebugStateChange &&
+        tabId == other.tabId &&
+        newState == other.newState &&
+        reason == other.reason;
+  }
+
+  @override
+  int get hashCode {
+    return $jf(
+        $jc($jc($jc(0, tabId.hashCode), newState.hashCode), reason.hashCode));
+  }
+
+  @override
+  String toString() {
+    return (newBuiltValueToStringHelper(r'DebugStateChange')
+          ..add('tabId', tabId)
+          ..add('newState', newState)
+          ..add('reason', reason))
+        .toString();
+  }
+}
+
+class DebugStateChangeBuilder
+    implements Builder<DebugStateChange, DebugStateChangeBuilder> {
+  _$DebugStateChange? _$v;
+
+  int? _tabId;
+  int? get tabId => _$this._tabId;
+  set tabId(int? tabId) => _$this._tabId = tabId;
+
+  String? _newState;
+  String? get newState => _$this._newState;
+  set newState(String? newState) => _$this._newState = newState;
+
+  String? _reason;
+  String? get reason => _$this._reason;
+  set reason(String? reason) => _$this._reason = reason;
+
+  DebugStateChangeBuilder();
+
+  DebugStateChangeBuilder get _$this {
+    final $v = _$v;
+    if ($v != null) {
+      _tabId = $v.tabId;
+      _newState = $v.newState;
+      _reason = $v.reason;
+      _$v = null;
+    }
+    return this;
+  }
+
+  @override
+  void replace(DebugStateChange other) {
+    ArgumentError.checkNotNull(other, 'other');
+    _$v = other as _$DebugStateChange;
+  }
+
+  @override
+  void update(void Function(DebugStateChangeBuilder)? updates) {
+    if (updates != null) updates(this);
+  }
+
+  @override
+  DebugStateChange build() => _build();
+
+  _$DebugStateChange _build() {
+    final _$result = _$v ??
+        new _$DebugStateChange._(
+            tabId: BuiltValueNullFieldError.checkNotNull(
+                tabId, r'DebugStateChange', 'tabId'),
+            newState: BuiltValueNullFieldError.checkNotNull(
+                newState, r'DebugStateChange', 'newState'),
+            reason: reason);
+    replace(_$result);
+    return _$result;
+  }
+}
+
+// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas
diff --git a/dwds/debug_extension_mv3/web/debug_info.dart b/dwds/debug_extension_mv3/web/debug_info.dart
new file mode 100644
index 0000000..b827a41
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/debug_info.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library debug_info;
+
+import 'dart:convert';
+import 'dart:html';
+import 'dart:js';
+
+import 'package:dwds/data/debug_info.dart';
+import 'package:dwds/data/serializers.dart';
+import 'package:js/js.dart';
+
+void main() {
+  final debugInfoJson = _readDartDebugInfo();
+  document.dispatchEvent(CustomEvent('dart-app-ready', detail: debugInfoJson));
+}
+
+String _readDartDebugInfo() {
+  final windowContext = JsObject.fromBrowserObject(window);
+
+  return jsonEncode(serializers.serialize(DebugInfo((b) => b
+    ..appEntrypointPath = windowContext['\$dartEntrypointPath']
+    ..appId = windowContext['\$dartAppId']
+    ..appInstanceId = windowContext['\$dartAppInstanceId']
+    ..appOrigin = window.location.origin
+    ..appUrl = window.location.href
+    ..extensionUrl = windowContext['\$dartExtensionUri']
+    ..isInternalBuild = windowContext['\$isInternalBuild']
+    ..isFlutterApp = windowContext['\$isFlutterApp'])));
+}
diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart
new file mode 100644
index 0000000..8c19869
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/debug_session.dart
@@ -0,0 +1,490 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library debug_session;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:html';
+
+import 'package:built_collection/built_collection.dart';
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:dwds/data/debug_info.dart';
+import 'package:dwds/data/devtools_request.dart';
+import 'package:dwds/data/extension_request.dart';
+import 'package:dwds/src/sockets.dart';
+// TODO(https://github.com/dart-lang/sdk/issues/49973): Use conditional imports
+// in .../utilities/batched_stream so that we don't need to import a copy.
+import 'package:dwds/src/web_utilities/batched_stream.dart';
+import 'package:js/js.dart';
+import 'package:js/js_util.dart' as js_util;
+import 'package:sse/client/sse_client.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import 'chrome_api.dart';
+import 'cross_extension_communication.dart';
+import 'data_serializers.dart';
+import 'data_types.dart';
+import 'logger.dart';
+import 'messaging.dart';
+import 'storage.dart';
+import 'utils.dart';
+import 'web_api.dart';
+
+const _notADartAppAlert = 'No Dart application detected.'
+    ' Are you trying to debug an application that includes a Chrome hosted app'
+    ' (an application listed in chrome://apps)? If so, debugging is disabled.'
+    ' You can fix this by removing the application from chrome://apps. Please'
+    ' see https://bugs.chromium.org/p/chromium/issues/detail?id=885025#c11.';
+
+const _devToolsAlreadyOpenedAlert =
+    'DevTools is already opened on a different window.';
+
+final _debugSessions = <_DebugSession>[];
+final _tabIdToTrigger = <int, Trigger>{};
+
+enum DetachReason {
+  canceledByUser,
+  connectionErrorEvent,
+  connectionDoneEvent,
+  devToolsTabClosed,
+  navigatedAwayFromApp,
+  unknown;
+
+  factory DetachReason.fromString(String value) {
+    return DetachReason.values.byName(value);
+  }
+}
+
+enum ConnectFailureReason {
+  authentication,
+  noDartApp,
+  timeout,
+  unknown;
+
+  factory ConnectFailureReason.fromString(String value) {
+    return ConnectFailureReason.values.byName(value);
+  }
+}
+
+enum TabType {
+  dartApp,
+  devTools,
+}
+
+enum Trigger {
+  angularDartDevTools,
+  extensionPanel,
+  extensionIcon,
+}
+
+void attachDebugger(int dartAppTabId, {required Trigger trigger}) {
+  _tabIdToTrigger[dartAppTabId] = trigger;
+  _registerDebugEventListeners();
+  chrome.debugger.attach(
+    Debuggee(tabId: dartAppTabId),
+    '1.3',
+    allowInterop(
+      () => _enableExecutionContextReporting(dartAppTabId),
+    ),
+  );
+}
+
+void detachDebugger(
+  int tabId, {
+  required TabType type,
+  required DetachReason reason,
+}) async {
+  final debugSession = _debugSessionForTab(tabId, type: type);
+  if (debugSession == null) return;
+  final debuggee = Debuggee(tabId: debugSession.appTabId);
+  final detachPromise = chrome.debugger.detach(debuggee);
+  await promiseToFuture(detachPromise);
+  final error = chrome.runtime.lastError;
+  if (error != null) {
+    debugWarn(
+        'Error detaching tab for reason: $reason. Error: ${error.message}');
+  } else {
+    _handleDebuggerDetach(debuggee, reason);
+  }
+}
+
+void _registerDebugEventListeners() {
+  chrome.debugger.onEvent.addListener(allowInterop(_onDebuggerEvent));
+  chrome.debugger.onDetach.addListener(allowInterop(
+    (source, _) => _handleDebuggerDetach(
+      source,
+      DetachReason.canceledByUser,
+    ),
+  ));
+  chrome.tabs.onRemoved.addListener(allowInterop(
+    (tabId, _) => detachDebugger(
+      tabId,
+      type: TabType.devTools,
+      reason: DetachReason.devToolsTabClosed,
+    ),
+  ));
+}
+
+_enableExecutionContextReporting(int tabId) {
+  // Runtime.enable enables reporting of execution contexts creation by means of
+  // executionContextCreated event. When the reporting gets enabled the event
+  // will be sent immediately for each existing execution context:
+  chrome.debugger.sendCommand(
+      Debuggee(tabId: tabId), 'Runtime.enable', EmptyParam(), allowInterop((_) {
+    final chromeError = chrome.runtime.lastError;
+    if (chromeError != null) {
+      final errorMessage = _translateChromeError(chromeError.message);
+      chrome.notifications.create(/*notificationId*/ null,
+          NotificationOptions(message: errorMessage), /*callback*/ null);
+      return;
+    }
+  }));
+}
+
+String _translateChromeError(String chromeErrorMessage) {
+  if (chromeErrorMessage.contains('Cannot access') ||
+      chromeErrorMessage.contains('Cannot attach')) {
+    return _notADartAppAlert;
+  }
+  return _devToolsAlreadyOpenedAlert;
+}
+
+Future<void> _onDebuggerEvent(
+    Debuggee source, String method, Object? params) async {
+  maybeForwardMessageToAngularDartDevTools(
+      method: method, params: params, tabId: source.tabId);
+
+  if (method == 'Runtime.executionContextCreated') {
+    return _maybeConnectToDwds(source.tabId, params);
+  }
+
+  return _forwardChromeDebuggerEventToDwds(source, method, params);
+}
+
+Future<void> _maybeConnectToDwds(int tabId, Object? params) async {
+  final context = json.decode(JSON.stringify(params))['context'];
+  final contextOrigin = context['origin'] as String?;
+  if (contextOrigin == null) return;
+  if (contextOrigin.contains(('chrome-extension:'))) return;
+  final debugInfo = await fetchStorageObject<DebugInfo>(
+    type: StorageObject.debugInfo,
+    tabId: tabId,
+  );
+  if (debugInfo == null) return;
+  if (contextOrigin != debugInfo.appOrigin) return;
+  final contextId = context['id'] as int;
+  final connected = await _connectToDwds(
+    dartAppContextId: contextId,
+    dartAppTabId: tabId,
+    debugInfo: debugInfo,
+  );
+  if (!connected) {
+    debugWarn('Failed to connect to DWDS for $contextOrigin.');
+    sendConnectFailureMessage(ConnectFailureReason.unknown,
+        dartAppTabId: tabId);
+  }
+}
+
+Future<bool> _connectToDwds({
+  required int dartAppContextId,
+  required int dartAppTabId,
+  required DebugInfo debugInfo,
+}) async {
+  if (debugInfo.extensionUrl == null) {
+    debugWarn('Can\'t connect to DWDS without an extension URL.');
+    return false;
+  }
+  final uri = Uri.parse(debugInfo.extensionUrl!);
+  // Start the client connection with DWDS:
+  final client = uri.isScheme('ws') || uri.isScheme('wss')
+      ? WebSocketClient(WebSocketChannel.connect(uri))
+      : SseSocketClient(SseClient(uri.toString(), debugKey: 'DebugExtension'));
+  final trigger = _tabIdToTrigger[dartAppTabId];
+  final debugSession = _DebugSession(
+    client: client,
+    appTabId: dartAppTabId,
+    trigger: trigger,
+    onIncoming: (data) => _routeDwdsEvent(data, client, dartAppTabId),
+    onDone: () {
+      detachDebugger(
+        dartAppTabId,
+        type: TabType.dartApp,
+        reason: DetachReason.connectionDoneEvent,
+      );
+    },
+    onError: (err) {
+      debugWarn('Connection error: $err', verbose: true);
+      detachDebugger(
+        dartAppTabId,
+        type: TabType.dartApp,
+        reason: DetachReason.connectionErrorEvent,
+      );
+    },
+    cancelOnError: true,
+  );
+  _debugSessions.add(debugSession);
+  final tabUrl = await _getTabUrl(dartAppTabId);
+  // Send a DevtoolsRequest to the event stream:
+  debugSession.sendEvent(DevToolsRequest((b) => b
+    ..appId = debugInfo.appId
+    ..instanceId = debugInfo.appInstanceId
+    ..contextId = dartAppContextId
+    ..tabUrl = tabUrl
+    ..uriOnly = true));
+  return true;
+}
+
+void _routeDwdsEvent(String eventData, SocketClient client, int tabId) {
+  final message = serializers.deserialize(jsonDecode(eventData));
+  if (message is ExtensionRequest) {
+    _forwardDwdsEventToChromeDebugger(message, client, tabId);
+  } else if (message is ExtensionEvent) {
+    maybeForwardMessageToAngularDartDevTools(
+        method: message.method, params: message.params, tabId: tabId);
+    if (message.method == 'dwds.devtoolsUri') {
+      _openDevTools(message.params, dartAppTabId: tabId);
+    }
+    if (message.method == 'dwds.encodedUri') {
+      setStorageObject(
+        type: StorageObject.encodedUri,
+        value: message.params,
+        tabId: tabId,
+      );
+    }
+  }
+}
+
+void _forwardDwdsEventToChromeDebugger(
+    ExtensionRequest message, SocketClient client, int tabId) {
+  final messageParams = message.commandParams ?? '{}';
+  final params = BuiltMap<String, Object>(json.decode(messageParams)).toMap();
+  chrome.debugger.sendCommand(
+      Debuggee(tabId: tabId), message.command, js_util.jsify(params),
+      allowInterop(([e]) {
+    // No arguments indicate that an error occurred.
+    if (e == null) {
+      client.sink
+          .add(jsonEncode(serializers.serialize(ExtensionResponse((b) => b
+            ..id = message.id
+            ..success = false
+            ..result = JSON.stringify(chrome.runtime.lastError)))));
+    } else {
+      client.sink
+          .add(jsonEncode(serializers.serialize(ExtensionResponse((b) => b
+            ..id = message.id
+            ..success = true
+            ..result = JSON.stringify(e)))));
+    }
+  }));
+}
+
+void _forwardChromeDebuggerEventToDwds(
+    Debuggee source, String method, dynamic params) {
+  final debugSession = _debugSessions
+      .firstWhereOrNull((session) => session.appTabId == source.tabId);
+  if (debugSession == null) return;
+  final event = _extensionEventFor(method, params);
+  if (method == 'Debugger.scriptParsed') {
+    debugSession.sendBatchedEvent(event);
+  } else {
+    debugSession.sendEvent(event);
+  }
+}
+
+void _openDevTools(String devToolsUri, {required int dartAppTabId}) async {
+  if (devToolsUri.isEmpty) {
+    debugError('DevTools URI is empty.');
+    return;
+  }
+  final debugSession = _debugSessionForTab(dartAppTabId, type: TabType.dartApp);
+  if (debugSession == null) {
+    debugError('Debug session not found.');
+    return;
+  }
+  // Save the DevTools URI so that the extension panels have access to it:
+  await setStorageObject(
+    type: StorageObject.devToolsUri,
+    value: devToolsUri,
+    tabId: dartAppTabId,
+  );
+  // Open a separate tab / window if triggered through the extension icon or
+  // through AngularDart DevTools:
+  if (debugSession.trigger == Trigger.extensionIcon ||
+      debugSession.trigger == Trigger.angularDartDevTools) {
+    final devToolsOpener = await fetchStorageObject<DevToolsOpener>(
+        type: StorageObject.devToolsOpener);
+    final devToolsTab = await createTab(
+      devToolsUri,
+      inNewWindow: devToolsOpener?.newWindow ?? false,
+    );
+    debugSession.devToolsTabId = devToolsTab.id;
+  }
+}
+
+void _handleDebuggerDetach(Debuggee source, DetachReason reason) async {
+  final tabId = source.tabId;
+  debugLog(
+    'Debugger detached due to: $reason',
+    verbose: true,
+    prefix: '$tabId',
+  );
+  final debugSession = _debugSessionForTab(tabId, type: TabType.dartApp);
+  if (debugSession == null) return;
+  debugLog('Removing debug session...');
+  _removeDebugSession(debugSession);
+  // Notify the extension panels that the debug session has ended:
+  _sendStopDebuggingMessage(reason, dartAppTabId: source.tabId);
+  // Remove the DevTools URI and encoded URI from storage:
+  await removeStorageObject(type: StorageObject.devToolsUri, tabId: tabId);
+  await removeStorageObject(type: StorageObject.encodedUri, tabId: tabId);
+  // Maybe close the associated DevTools tab as well:
+  final devToolsTabId = debugSession.devToolsTabId;
+  if (devToolsTabId == null) return;
+  final devToolsTab = await getTab(devToolsTabId);
+  if (devToolsTab != null) {
+    debugLog('Closing DevTools tab...');
+    chrome.tabs.remove(devToolsTabId);
+  }
+}
+
+void _removeDebugSession(_DebugSession debugSession) {
+  // Note: package:sse will try to keep the connection alive, even after the
+  // client has been closed. Therefore the extension sends an event to notify
+  // DWDS that we should close the connection, instead of relying on the done
+  // event sent when the client is closed. See details:
+  // https://github.com/dart-lang/webdev/pull/1595#issuecomment-1116773378
+  final event =
+      _extensionEventFor('DebugExtension.detached', js_util.jsify({}));
+  debugSession.sendEvent(event);
+  debugSession.close();
+  final removed = _debugSessions.remove(debugSession);
+  if (!removed) {
+    debugWarn('Could not remove debug session.');
+  }
+}
+
+void sendConnectFailureMessage(ConnectFailureReason reason,
+    {required int dartAppTabId}) async {
+  final json = jsonEncode(serializers.serialize(ConnectFailure((b) => b
+    ..tabId = dartAppTabId
+    ..reason = reason.name)));
+  sendRuntimeMessage(
+      type: MessageType.connectFailure,
+      body: json,
+      sender: Script.background,
+      recipient: Script.debuggerPanel);
+}
+
+void _sendStopDebuggingMessage(DetachReason reason,
+    {required int dartAppTabId}) async {
+  final json = jsonEncode(serializers.serialize(DebugStateChange((b) => b
+    ..tabId = dartAppTabId
+    ..reason = reason.name
+    ..newState = DebugStateChange.stopDebugging)));
+  sendRuntimeMessage(
+      type: MessageType.debugStateChange,
+      body: json,
+      sender: Script.background,
+      recipient: Script.debuggerPanel);
+}
+
+_DebugSession? _debugSessionForTab(tabId, {required TabType type}) {
+  switch (type) {
+    case TabType.dartApp:
+      return _debugSessions
+          .firstWhereOrNull((session) => session.appTabId == tabId);
+    case TabType.devTools:
+      return _debugSessions
+          .firstWhereOrNull((session) => session.devToolsTabId == tabId);
+  }
+}
+
+/// Construct an [ExtensionEvent] from [method] and [params].
+ExtensionEvent _extensionEventFor(String method, dynamic params) {
+  return ExtensionEvent((b) => b
+    ..params = jsonEncode(json.decode(JSON.stringify(params)))
+    ..method = jsonEncode(method));
+}
+
+Future<String> _getTabUrl(int tabId) async {
+  final tab = await getTab(tabId);
+  return tab?.url ?? '';
+}
+
+@JS()
+@anonymous
+class EmptyParam {
+  external factory EmptyParam();
+}
+
+class _DebugSession {
+  // The tab ID that contains the running Dart application.
+  final int appTabId;
+
+  // What triggered the debug session (debugger panel, extension icon, etc.)
+  final Trigger? trigger;
+
+  // Socket client for communication with dwds extension backend.
+  late final SocketClient _socketClient;
+
+  // How often to send batched events.
+  static const int _batchDelayMilliseconds = 1000;
+
+  // The tab ID that contains the corresponding Dart DevTools, if it exists.
+  int? devToolsTabId;
+
+  // Collect events into batches to be send periodically to the server.
+  final _batchController =
+      BatchedStreamController<ExtensionEvent>(delay: _batchDelayMilliseconds);
+  late final StreamSubscription<List<ExtensionEvent>> _batchSubscription;
+
+  _DebugSession({
+    required client,
+    required this.appTabId,
+    required this.trigger,
+    required void Function(String data) onIncoming,
+    required void Function() onDone,
+    required void Function(dynamic error) onError,
+    required bool cancelOnError,
+  }) : _socketClient = client {
+    // Collect extension events and send them periodically to the server.
+    _batchSubscription = _batchController.stream.listen((events) {
+      _socketClient.sink.add(jsonEncode(serializers.serialize(BatchedEvents(
+          (b) => b.events = ListBuilder<ExtensionEvent>(events)))));
+    });
+    // Listen for incoming events:
+    _socketClient.stream.listen(
+      onIncoming,
+      onDone: onDone,
+      onError: onError,
+      cancelOnError: cancelOnError,
+    );
+  }
+
+  set socketClient(SocketClient client) {
+    _socketClient = client;
+
+    // Collect extension events and send them periodically to the server.
+    _batchSubscription = _batchController.stream.listen((events) {
+      _socketClient.sink.add(jsonEncode(serializers.serialize(BatchedEvents(
+          (b) => b.events = ListBuilder<ExtensionEvent>(events)))));
+    });
+  }
+
+  void sendEvent<T>(T event) {
+    _socketClient.sink.add(jsonEncode(serializers.serialize(event)));
+  }
+
+  void sendBatchedEvent(ExtensionEvent event) {
+    _batchController.sink.add(event);
+  }
+
+  void close() {
+    _socketClient.close();
+    _batchSubscription.cancel();
+    _batchController.close();
+  }
+}
diff --git a/dwds/debug_extension_mv3/web/detector.dart b/dwds/debug_extension_mv3/web/detector.dart
index d30964e..7689983 100644
--- a/dwds/debug_extension_mv3/web/detector.dart
+++ b/dwds/debug_extension_mv3/web/detector.dart
@@ -6,9 +6,11 @@
 library detector;
 
 import 'dart:html';
+import 'dart:js_util';
 import 'package:js/js.dart';
 
 import 'chrome_api.dart';
+import 'logger.dart';
 import 'messaging.dart';
 
 void main() {
@@ -20,26 +22,38 @@
 }
 
 void _onDartAppReadyEvent(Event event) {
-  _sendMessageToBackgroundScript(
-    type: MessageType.dartAppReady,
-    body: 'Dart app ready!',
-  );
+  final debugInfo = getProperty(event, 'detail') as String?;
+  if (debugInfo == null) {
+    debugWarn(
+        'No debug info sent with ready event, instead reading from Window.');
+    _injectDebugInfoScript();
+  } else {
+    _sendMessageToBackgroundScript(
+      type: MessageType.debugInfo,
+      body: debugInfo,
+    );
+  }
+}
+
+// TODO(elliette): Remove once DWDS 17.0.0 is in Flutter stable. If we are on an
+// older version of DWDS, then the debug info is not sent along with the ready
+// event. Therefore we must read it from the Window object, which is slower.
+void _injectDebugInfoScript() {
+  final script = document.createElement('script');
+  final scriptSrc = chrome.runtime.getURL('debug_info.dart.js');
+  script.setAttribute('src', scriptSrc);
+  script.setAttribute('defer', true);
+  document.head?.append(script);
 }
 
 void _sendMessageToBackgroundScript({
   required MessageType type,
   required String body,
 }) {
-  final message = Message(
-    to: Script.background,
-    from: Script.detector,
+  sendRuntimeMessage(
     type: type,
     body: body,
-  );
-  chrome.runtime.sendMessage(
-    /*id*/ null,
-    message.toJSON(),
-    /*options*/ null,
-    /*callback*/ null,
+    sender: Script.detector,
+    recipient: Script.background,
   );
 }
diff --git a/dwds/debug_extension_mv3/web/devtools.dart b/dwds/debug_extension_mv3/web/devtools.dart
new file mode 100644
index 0000000..a8b37b3
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/devtools.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library devtools;
+
+import 'dart:html';
+import 'package:js/js.dart';
+import 'package:dwds/data/debug_info.dart';
+
+import 'chrome_api.dart';
+import 'logger.dart';
+import 'storage.dart';
+import 'utils.dart';
+
+bool panelsExist = false;
+
+void main() async {
+  _registerListeners();
+  _maybeCreatePanels();
+}
+
+void _registerListeners() {
+  chrome.storage.onChanged.addListener(allowInterop((
+    Object _,
+    String storageArea,
+  ) {
+    if (storageArea != 'session') return;
+    _maybeCreatePanels();
+  }));
+}
+
+void _maybeCreatePanels() async {
+  if (panelsExist) return;
+  final tabId = chrome.devtools.inspectedWindow.tabId;
+  final debugInfo = await fetchStorageObject<DebugInfo>(
+    type: StorageObject.debugInfo,
+    tabId: tabId,
+  );
+  if (debugInfo == null) return;
+  final isInternalBuild = debugInfo.isInternalBuild ?? false;
+  if (!isInternalBuild) return;
+  // Create a Debugger panel for all internal apps:
+  chrome.devtools.panels.create(
+    isDevMode() ? '[DEV] Dart Debugger' : 'Dart Debugger',
+    '',
+    'static_assets/debugger_panel.html',
+    allowInterop((ExtensionPanel panel) => _onPanelAdded(panel, debugInfo)),
+  );
+  // Create an inspector panel for internal Flutter apps:
+  final isFlutterApp = debugInfo.isFlutterApp ?? false;
+  if (isFlutterApp) {
+    chrome.devtools.panels.create(
+      isDevMode() ? '[DEV] Flutter Inspector' : 'Flutter Inspector',
+      '',
+      'static_assets/inspector_panel.html',
+      allowInterop((ExtensionPanel panel) => _onPanelAdded(panel, debugInfo)),
+    );
+  }
+  panelsExist = true;
+}
+
+void _onPanelAdded(ExtensionPanel panel, DebugInfo debugInfo) {
+  panel.onShown.addListener(allowInterop((Window window) {
+    if (window.origin != debugInfo.appOrigin) {
+      debugWarn('Page at ${window.origin} is no longer a Dart app.');
+      // TODO(elliette): Display banner that panel is not applicable. See:
+      // https://stackoverflow.com/questions/18927147/how-to-close-destroy-chrome-devtools-extensionpanel-programmatically
+    }
+  }));
+}
diff --git a/dwds/debug_extension_mv3/web/lifeline_connection.dart b/dwds/debug_extension_mv3/web/lifeline_connection.dart
new file mode 100644
index 0000000..3096a95
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/lifeline_connection.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'chrome_api.dart';
+import 'logger.dart';
+
+void main() async {
+  _connectToLifelinePort();
+}
+
+void _connectToLifelinePort() {
+  debugLog(
+    'Connecting to lifeline port at ${_currentTime()}.',
+    prefix: 'Dart Debug Extension',
+  );
+  chrome.runtime.connect(
+    /*extensionId=*/ null,
+    ConnectInfo(name: 'keepAlive'),
+  );
+}
+
+String _currentTime() {
+  final date = DateTime.now();
+  return '${date.hour}:${date.minute}::${date.second}';
+}
diff --git a/dwds/debug_extension_mv3/web/lifeline_ports.dart b/dwds/debug_extension_mv3/web/lifeline_ports.dart
new file mode 100644
index 0000000..23f57ed
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/lifeline_ports.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Keeps the background service worker alive for the duration of a Dart debug
+// session by using the workaround described in:
+// https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c21
+@JS()
+library lifeline_ports;
+
+import 'dart:async';
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'logger.dart';
+
+// Switch to true to enable debug logs.
+// TODO(elliette): Enable / disable with flag while building the extension.
+final enableDebugLogging = true;
+
+Port? lifelinePort;
+int? lifelineTab;
+final dartTabs = <int>{};
+
+void maybeCreateLifelinePort(int tabId) {
+  // Keep track of current Dart tabs that are being debugged. This way if one of
+  // them is closed, we can reconnect the lifeline port to another one:
+  dartTabs.add(tabId);
+  debugLog('Dart tabs are: $dartTabs');
+  // Don't create a lifeline port if we already have one (meaning another Dart
+  // app is currently being debugged):
+  if (lifelinePort != null) {
+    debugWarn('Port already exists.');
+    return;
+  }
+  // Start the keep-alive logic when the port connects:
+  chrome.runtime.onConnect.addListener(allowInterop(_keepLifelinePortAlive));
+  // Inject the connection script into the current Dart tab, that way the tab
+  // will connect to the port:
+  debugLog('Creating lifeline port.');
+  lifelineTab = tabId;
+  chrome.scripting.executeScript(
+    InjectDetails(
+      target: Target(tabId: tabId),
+      files: ['lifeline_connection.dart.js'],
+    ),
+    /*callback*/ null,
+  );
+}
+
+void maybeRemoveLifelinePort(int removedTabId) {
+  final removedDartTab = dartTabs.remove(removedTabId);
+  // If the removed tab was not a Dart tab, return early.
+  if (!removedDartTab) return;
+  debugLog('Removed tab $removedTabId, Dart tabs are now $dartTabs.');
+  // If the removed Dart tab hosted the lifeline port connection, see if there
+  // are any other Dart tabs to connect to. Otherwise disconnect the port.
+  if (lifelineTab == removedTabId) {
+    if (dartTabs.isEmpty) {
+      lifelineTab = null;
+      debugLog('No more Dart tabs, disconnecting from lifeline port.');
+      _disconnectFromLifelinePort();
+    } else {
+      lifelineTab = dartTabs.last;
+      debugLog('Reconnecting lifeline port to a new Dart tab: $lifelineTab.');
+      _reconnectToLifelinePort();
+    }
+  }
+}
+
+void _keepLifelinePortAlive(Port port) {
+  final portName = port.name ?? '';
+  if (portName != 'keepAlive') return;
+  lifelinePort = port;
+  // Reconnect to the lifeline port every 5 minutes, as per:
+  // https://bugs.chromium.org/p/chromium/issues/detail?id=1146434#c6
+  Timer(Duration(minutes: 5), () {
+    debugLog('5 minutes have elapsed, therefore reconnecting.');
+    _reconnectToLifelinePort();
+  });
+}
+
+void _reconnectToLifelinePort() {
+  debugLog('Reconnecting...');
+  if (lifelinePort == null) {
+    debugWarn('Could not find a lifeline port.');
+    return;
+  }
+  if (lifelineTab == null) {
+    debugWarn('Could not find a lifeline tab.');
+    return;
+  }
+  // Disconnect from the port, and then recreate the connection with the current
+  // Dart tab:
+  _disconnectFromLifelinePort();
+  maybeCreateLifelinePort(lifelineTab!);
+  debugLog('Reconnection complete.');
+}
+
+void _disconnectFromLifelinePort() {
+  debugLog('Disconnecting...');
+  if (lifelinePort != null) {
+    lifelinePort!.disconnect();
+    lifelinePort = null;
+    debugLog('Disconnection complete.');
+  }
+}
diff --git a/dwds/debug_extension_mv3/web/logger.dart b/dwds/debug_extension_mv3/web/logger.dart
new file mode 100644
index 0000000..d0599ec
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/logger.dart
@@ -0,0 +1,77 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library logger;
+
+import 'package:js/js.dart';
+
+import 'utils.dart';
+
+enum _LogLevel {
+  info,
+  warn,
+  error,
+}
+
+debugLog(
+  String msg, {
+  String? prefix,
+  bool verbose = false,
+}) {
+  _log(msg, prefix: prefix, verbose: verbose);
+}
+
+debugWarn(
+  String msg, {
+  String? prefix,
+  bool verbose = false,
+}) {
+  _log(msg, prefix: prefix, level: _LogLevel.warn, verbose: verbose);
+}
+
+debugError(
+  String msg, {
+  String? prefix,
+  bool verbose = false,
+}) {
+  _log(msg, prefix: prefix, level: _LogLevel.error, verbose: verbose);
+}
+
+void _log(
+  String msg, {
+  bool verbose = false,
+  _LogLevel? level,
+  String? prefix,
+}) {
+  if (!verbose && !isDevMode()) return;
+  final logMsg = prefix != null ? '[$prefix] $msg' : msg;
+  final logLevel = level ?? _LogLevel.info;
+  switch (logLevel) {
+    case _LogLevel.error:
+      _console.error(logMsg);
+      break;
+    case _LogLevel.warn:
+      _console.warn(logMsg);
+      break;
+    case _LogLevel.info:
+      _console.log(logMsg);
+  }
+}
+
+@JS('console')
+external _Console get _console;
+
+@JS()
+@anonymous
+class _Console {
+  external void log(String header,
+      [String style1, String style2, String style3]);
+
+  external void warn(String header,
+      [String style1, String style2, String style3]);
+
+  external void error(String header,
+      [String style1, String style2, String style3]);
+}
diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest.json
index 2492e46..d16a32e 100644
--- a/dwds/debug_extension_mv3/web/manifest.json
+++ b/dwds/debug_extension_mv3/web/manifest.json
@@ -1,41 +1,38 @@
 {
-    "name": "[MV3] Dart Debug Extension",
-    "version": "1.0",
-    "manifest_version": 3,
-    "action": {
-        "default_icon": "dart_dev.png"
-    },
-    "permissions": [
-        "scripting",
-        "tabs",
-        "debugger"
-    ],
-    "host_permissions": [
-        "<all_urls>"
-    ],
-    "web_accessible_resources": [
-        {
-            "matches": [
-                "<all_urls>"
-            ],
-            "resources": [
-                "iframe.html",
-                "iframe_injector.dart.js"
-            ]
-        }
-    ],
-    "background": {
-        "service_worker": "background.dart.js"
-    },
-    "content_scripts": [
-        {
-            "matches": [
-                "<all_urls>"
-            ],
-            "js": [
-                "detector.dart.js"
-            ],
-            "run_at": "document_end"
-        }
-    ]
-}
\ No newline at end of file
+  "name": "Dart Debug Extension",
+  "version": "1.31",
+  "manifest_version": 3,
+  "devtools_page": "static_assets/devtools.html",
+  "action": {
+    "default_icon": "static_assets/dart_dev.png"
+  },
+  "externally_connectable": {
+    "ids": ["nbkbficgbembimioedhceniahniffgpl"]
+  },
+  "permissions": [
+    "debugger",
+    "notifications",
+    "scripting",
+    "storage",
+    "tabs",
+    "webNavigation"
+  ],
+  "host_permissions": ["<all_urls>"],
+  "background": {
+    "service_worker": "background.dart.js"
+  },
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "js": ["detector.dart.js"],
+      "run_at": "document_end"
+    }
+  ],
+  "web_accessible_resources": [
+    {
+      "matches": ["<all_urls>"],
+      "resources": ["debug_info.dart.js"]
+    }
+  ],
+  "options_page": "static_assets/settings.html"
+}
diff --git a/dwds/debug_extension_mv3/web/messaging.dart b/dwds/debug_extension_mv3/web/messaging.dart
index 08128a1..5aa551a 100644
--- a/dwds/debug_extension_mv3/web/messaging.dart
+++ b/dwds/debug_extension_mv3/web/messaging.dart
@@ -9,10 +9,13 @@
 
 import 'package:js/js.dart';
 
-import 'web_api.dart';
+import 'data_serializers.dart';
+import 'chrome_api.dart';
+import 'logger.dart';
 
 enum Script {
   background,
+  debuggerPanel,
   detector;
 
   factory Script.fromString(String value) {
@@ -21,7 +24,10 @@
 }
 
 enum MessageType {
-  dartAppReady;
+  connectFailure,
+  debugInfo,
+  debugStateChange,
+  devToolsUrl;
 
   factory MessageType.fromString(String value) {
     return MessageType.values.byName(value);
@@ -57,34 +63,53 @@
 
   String toJSON() {
     return jsonEncode({
-      'type': type.name,
       'to': to.name,
       'from': from.name,
-      'encodedBody': body,
+      'type': type.name,
+      'body': body,
       if (error != null) 'error': error,
     });
   }
 }
 
-void interceptMessage({
+void interceptMessage<T>({
   required String? message,
   required MessageType expectedType,
   required Script expectedSender,
   required Script expectedRecipient,
-  required void Function(String message) messageHandler,
+  required void Function(T message) messageHandler,
 }) {
+  if (message == null) return;
   try {
-    if (message == null) return;
     final decodedMessage = Message.fromJSON(message);
     if (decodedMessage.type != expectedType ||
         decodedMessage.to != expectedRecipient ||
         decodedMessage.from != expectedSender) {
       return;
     }
-    messageHandler(decodedMessage.body);
+    messageHandler(
+        serializers.deserialize(jsonDecode(decodedMessage.body)) as T);
   } catch (error) {
-    console.warn(
-        'Error intercepting $expectedType message from $expectedSender to $expectedRecipient: $error');
-    return;
+    debugError(
+        'Error intercepting $expectedType from $expectedSender to $expectedRecipient: $error');
   }
 }
+
+void sendRuntimeMessage(
+    {required MessageType type,
+    required String body,
+    required Script sender,
+    required Script recipient}) {
+  final message = Message(
+    to: recipient,
+    from: sender,
+    type: type,
+    body: body,
+  );
+  chrome.runtime.sendMessage(
+    /*id*/ null,
+    message.toJSON(),
+    /*options*/ null,
+    /*callback*/ null,
+  );
+}
diff --git a/dwds/debug_extension_mv3/web/panel.dart b/dwds/debug_extension_mv3/web/panel.dart
new file mode 100644
index 0000000..d2ed9a6
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/panel.dart
@@ -0,0 +1,275 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library panel;
+
+import 'dart:convert';
+import 'dart:html';
+
+import 'package:dwds/data/debug_info.dart';
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'data_serializers.dart';
+import 'data_types.dart';
+import 'debug_session.dart';
+import 'logger.dart';
+import 'messaging.dart';
+import 'storage.dart';
+
+bool connecting = false;
+String devToolsBackgroundColor = darkColor;
+bool isDartApp = true;
+
+const bugLinkId = 'bugLink';
+const darkColor = '202125';
+const darkThemeClass = 'dark-theme';
+const hiddenClass = 'hidden';
+const iframeContainerId = 'iframeContainer';
+const landingPageId = 'landingPage';
+const launchDebugConnectionButtonId = 'launchDebugConnectionButton';
+const lightColor = 'ffffff';
+const lightThemeClass = 'light-theme';
+const loadingSpinnerId = 'loadingSpinner';
+const panelAttribute = 'data-panel';
+const panelBodyId = 'panelBody';
+const showClass = 'show';
+const warningBannerId = 'warningBanner';
+const warningMsgId = 'warningMsg';
+
+int get _tabId => chrome.devtools.inspectedWindow.tabId;
+
+void main() {
+  _registerListeners();
+  _setColorThemeToMatchChromeDevTools();
+  _maybeUpdateFileABugLink();
+}
+
+void _registerListeners() {
+  chrome.storage.onChanged.addListener(allowInterop(_handleStorageChanges));
+  chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
+  final launchDebugConnectionButton =
+      document.getElementById(launchDebugConnectionButtonId) as ButtonElement;
+  launchDebugConnectionButton.addEventListener('click', _launchDebugConnection);
+
+  _maybeInjectDevToolsIframe();
+}
+
+void _handleRuntimeMessages(
+    dynamic jsRequest, MessageSender sender, Function sendResponse) async {
+  if (jsRequest is! String) return;
+
+  interceptMessage<DebugStateChange>(
+      message: jsRequest,
+      expectedType: MessageType.debugStateChange,
+      expectedSender: Script.background,
+      expectedRecipient: Script.debuggerPanel,
+      messageHandler: (DebugStateChange debugStateChange) async {
+        if (debugStateChange.tabId != _tabId) {
+          debugWarn(
+              'Received debug state change request, but Dart app tab does not match current tab.');
+          return;
+        }
+        if (debugStateChange.newState == DebugStateChange.stopDebugging) {
+          _handleDebugConnectionLost(debugStateChange.reason);
+        }
+      });
+
+  interceptMessage<ConnectFailure>(
+      message: jsRequest,
+      expectedType: MessageType.connectFailure,
+      expectedSender: Script.background,
+      expectedRecipient: Script.debuggerPanel,
+      messageHandler: (ConnectFailure connectFailure) async {
+        debugLog(
+            'Received connect failure for ${connectFailure.tabId} vs $_tabId');
+        if (connectFailure.tabId != _tabId) {
+          return;
+        }
+        connecting = false;
+        _handleConnectFailure(
+          ConnectFailureReason.fromString(connectFailure.reason ?? 'unknown'),
+        );
+      });
+}
+
+void _handleStorageChanges(Object storageObj, String storageArea) {
+  // We only care about session storage objects:
+  if (storageArea != 'session') return;
+
+  interceptStorageChange<DebugInfo>(
+    storageObj: storageObj,
+    expectedType: StorageObject.debugInfo,
+    tabId: _tabId,
+    changeHandler: _handleDebugInfoChanges,
+  );
+  interceptStorageChange<String>(
+    storageObj: storageObj,
+    expectedType: StorageObject.devToolsUri,
+    tabId: _tabId,
+    changeHandler: _handleDevToolsUriChanges,
+  );
+}
+
+void _handleDebugInfoChanges(DebugInfo? debugInfo) async {
+  if (debugInfo == null && isDartApp) {
+    isDartApp = false;
+    _showWarningBanner('Dart app is no longer open.');
+  }
+  if (debugInfo != null && !isDartApp) {
+    isDartApp = true;
+    _hideWarningBanner();
+  }
+}
+
+void _handleDevToolsUriChanges(String? devToolsUri) async {
+  if (devToolsUri != null) {
+    _injectDevToolsIframe(devToolsUri);
+  }
+}
+
+void _maybeUpdateFileABugLink() async {
+  final debugInfo = await fetchStorageObject<DebugInfo>(
+    type: StorageObject.debugInfo,
+    tabId: _tabId,
+  );
+  final isInternal = debugInfo?.isInternalBuild ?? false;
+  if (isInternal) {
+    final bugLink = document.getElementById(bugLinkId);
+    if (bugLink == null) return;
+    bugLink.setAttribute(
+        'href', 'http://b/issues/new?component=775375&template=1369639');
+  }
+}
+
+void _setColorThemeToMatchChromeDevTools() async {
+  final chromeTheme = chrome.devtools.panels.themeName;
+  final panelBody = document.getElementById(panelBodyId);
+  if (chromeTheme == 'dark') {
+    devToolsBackgroundColor = darkColor;
+    _updateColorThemeForElement(panelBody, isDarkTheme: true);
+  } else {
+    devToolsBackgroundColor = lightColor;
+    _updateColorThemeForElement(panelBody, isDarkTheme: false);
+  }
+}
+
+void _updateColorThemeForElement(
+  Element? element, {
+  required bool isDarkTheme,
+}) {
+  if (element == null) return;
+  final classToRemove = isDarkTheme ? lightThemeClass : darkThemeClass;
+  if (element.classes.contains(classToRemove)) {
+    element.classes.remove(classToRemove);
+    final classToAdd = isDarkTheme ? darkThemeClass : lightThemeClass;
+    element.classes.add(classToAdd);
+  }
+}
+
+void _handleDebugConnectionLost(String? reason) {
+  final detachReason = DetachReason.fromString(reason ?? 'unknown');
+  _removeDevToolsIframe();
+  _updateElementVisibility(landingPageId, visible: true);
+  if (detachReason != DetachReason.canceledByUser) {
+    _showWarningBanner('Lost connection.');
+  }
+}
+
+void _handleConnectFailure(ConnectFailureReason reason) {
+  switch (reason) {
+    case ConnectFailureReason.authentication:
+      _showWarningBanner('Please re-authenticate and try again.');
+      break;
+    case ConnectFailureReason.noDartApp:
+      _showWarningBanner('No Dart app detected.');
+      break;
+    case ConnectFailureReason.timeout:
+      _showWarningBanner('Connection timed out.');
+      break;
+    default:
+      _showWarningBanner('Failed to connect, please try again.');
+  }
+  _updateElementVisibility(launchDebugConnectionButtonId, visible: true);
+  _updateElementVisibility(loadingSpinnerId, visible: false);
+}
+
+void _showWarningBanner(String message) {
+  final warningMsg = document.getElementById(warningMsgId);
+  warningMsg?.setInnerHtml(message);
+  print(warningMsg);
+  final warningBanner = document.getElementById(warningBannerId);
+  warningBanner?.classes.add(showClass);
+}
+
+void _hideWarningBanner() {
+  final warningBanner = document.getElementById(warningBannerId);
+  warningBanner?.classes.remove(showClass);
+}
+
+void _launchDebugConnection(Event _) async {
+  _updateElementVisibility(launchDebugConnectionButtonId, visible: false);
+  _updateElementVisibility(loadingSpinnerId, visible: true);
+  final json = jsonEncode(serializers.serialize(DebugStateChange((b) => b
+    ..tabId = _tabId
+    ..newState = DebugStateChange.startDebugging)));
+  sendRuntimeMessage(
+      type: MessageType.debugStateChange,
+      body: json,
+      sender: Script.debuggerPanel,
+      recipient: Script.background);
+  _maybeHandleConnectionTimeout();
+}
+
+void _maybeHandleConnectionTimeout() async {
+  connecting = true;
+  await Future.delayed(Duration(seconds: 10));
+  if (connecting == true) {
+    _handleConnectFailure(ConnectFailureReason.timeout);
+  }
+}
+
+void _maybeInjectDevToolsIframe() async {
+  final devToolsUri = await fetchStorageObject<String>(
+      type: StorageObject.devToolsUri, tabId: _tabId);
+  if (devToolsUri != null) {
+    _injectDevToolsIframe(devToolsUri);
+  }
+}
+
+void _injectDevToolsIframe(String devToolsUri) {
+  connecting = false;
+  final iframeContainer = document.getElementById(iframeContainerId);
+  if (iframeContainer == null) return;
+  final panelBody = document.getElementById(panelBodyId);
+  final panelType = panelBody?.getAttribute(panelAttribute) ?? 'debugger';
+  final iframe = document.createElement('iframe');
+  iframe.setAttribute(
+    'src',
+    '$devToolsUri&embed=true&page=$panelType&backgroundColor=$devToolsBackgroundColor',
+  );
+  _hideWarningBanner();
+  _updateElementVisibility(landingPageId, visible: false);
+  _updateElementVisibility(loadingSpinnerId, visible: false);
+  _updateElementVisibility(launchDebugConnectionButtonId, visible: true);
+  iframeContainer.append(iframe);
+}
+
+void _removeDevToolsIframe() {
+  final iframeContainer = document.getElementById(iframeContainerId);
+  final iframe = iframeContainer?.firstChild;
+  if (iframe == null) return;
+  iframe.remove();
+}
+
+void _updateElementVisibility(String elementId, {required bool visible}) {
+  final element = document.getElementById(elementId);
+  if (element == null) return;
+  if (visible) {
+    element.classes.remove(hiddenClass);
+  } else {
+    element.classes.add(hiddenClass);
+  }
+}
diff --git a/dwds/debug_extension_mv3/web/settings.dart b/dwds/debug_extension_mv3/web/settings.dart
new file mode 100644
index 0000000..450b190
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/settings.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library settings;
+
+import 'dart:async';
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+import 'data_types.dart';
+import 'storage.dart';
+
+void main() {
+  _registerListeners();
+}
+
+void _registerListeners() {
+  document.addEventListener('DOMContentLoaded', _updateSettingsFromStorage);
+  final saveButton = document.getElementById('saveButton') as ButtonElement;
+  saveButton.addEventListener('click', _saveSettingsToStorage);
+}
+
+void _updateSettingsFromStorage(Event _) async {
+  final devToolsOpener = await fetchStorageObject<DevToolsOpener>(
+      type: StorageObject.devToolsOpener);
+  final openInNewWindow = devToolsOpener?.newWindow ?? false;
+  _getRadioButton('windowOpt').checked = openInNewWindow;
+  _getRadioButton('tabOpt').checked = !openInNewWindow;
+}
+
+void _saveSettingsToStorage(Event event) async {
+  event.preventDefault();
+  _maybeHideSavedMsg();
+  final form = document.querySelector("form") as FormElement;
+  final data = FormData(form);
+  final devToolsOpenerValue = data.get('devToolsOpener') as String;
+  await setStorageObject<DevToolsOpener>(
+      type: StorageObject.devToolsOpener,
+      value: DevToolsOpener(
+          (b) => b..newWindow = devToolsOpenerValue == 'window'));
+  _showSavedMsg();
+}
+
+void _showSavedMsg() {
+  final snackbar = document.getElementById('savedSnackbar');
+  if (snackbar == null) return;
+  snackbar.classes.add('show');
+  Timer(Duration(seconds: 3), () {
+    _maybeHideSavedMsg();
+  });
+}
+
+void _maybeHideSavedMsg() {
+  final snackbar = document.getElementById('savedSnackbar');
+  if (snackbar == null) return;
+  snackbar.classes.remove('show');
+}
+
+RadioButtonInputElement _getRadioButton(String id) {
+  return document.getElementById(id) as RadioButtonInputElement;
+}
diff --git a/dwds/debug_extension_mv3/web/dart.png b/dwds/debug_extension_mv3/web/static_assets/dart.png
similarity index 100%
rename from dwds/debug_extension_mv3/web/dart.png
rename to dwds/debug_extension_mv3/web/static_assets/dart.png
Binary files differ
diff --git a/dwds/debug_extension_mv3/web/dart_dev.png b/dwds/debug_extension_mv3/web/static_assets/dart_dev.png
similarity index 100%
rename from dwds/debug_extension_mv3/web/dart_dev.png
rename to dwds/debug_extension_mv3/web/static_assets/dart_dev.png
Binary files differ
diff --git a/dwds/debug_extension_mv3/web/static_assets/dart_grey.png b/dwds/debug_extension_mv3/web/static_assets/dart_grey.png
new file mode 100644
index 0000000..3afba3f
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/static_assets/dart_grey.png
Binary files differ
diff --git a/dwds/debug_extension_mv3/web/static_assets/debugger_settings.png b/dwds/debug_extension_mv3/web/static_assets/debugger_settings.png
new file mode 100644
index 0000000..39a0cae
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/static_assets/debugger_settings.png
Binary files differ
diff --git a/dwds/debug_extension_mv3/web/static_assets/inspect_widget.png b/dwds/debug_extension_mv3/web/static_assets/inspect_widget.png
new file mode 100644
index 0000000..98f10cb
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/static_assets/inspect_widget.png
Binary files differ
diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart
new file mode 100644
index 0000000..d748027
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/storage.dart
@@ -0,0 +1,142 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library storage;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:js_util';
+
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'data_serializers.dart';
+import 'logger.dart';
+
+enum StorageObject {
+  debugInfo,
+  devToolsOpener,
+  devToolsUri,
+  encodedUri;
+
+  Persistance get persistance {
+    switch (this) {
+      case StorageObject.debugInfo:
+        return Persistance.sessionOnly;
+      case StorageObject.devToolsOpener:
+        return Persistance.acrossSessions;
+      case StorageObject.devToolsUri:
+        return Persistance.sessionOnly;
+      case StorageObject.encodedUri:
+        return Persistance.sessionOnly;
+    }
+  }
+}
+
+enum Persistance {
+  sessionOnly,
+  acrossSessions;
+}
+
+Future<bool> setStorageObject<T>({
+  required StorageObject type,
+  required T value,
+  int? tabId,
+  void Function()? callback,
+}) {
+  final storageKey = _createStorageKey(type, tabId);
+  final json =
+      value is String ? value : jsonEncode(serializers.serialize(value));
+  final storageObj = <String, String>{storageKey: json};
+  final completer = Completer<bool>();
+  final storageArea = _getStorageArea(type.persistance);
+  storageArea.set(jsify(storageObj), allowInterop(() {
+    if (callback != null) {
+      callback();
+    }
+    debugLog('Set: $json', prefix: storageKey);
+    completer.complete(true);
+  }));
+  return completer.future;
+}
+
+Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) {
+  final storageKey = _createStorageKey(type, tabId);
+  final completer = Completer<T?>();
+  final storageArea = _getStorageArea(type.persistance);
+  storageArea.get([storageKey], allowInterop((Object? storageObj) {
+    if (storageObj == null) {
+      debugWarn('Does not exist.', prefix: storageKey);
+      completer.complete(null);
+      return;
+    }
+    final json = getProperty(storageObj, storageKey) as String?;
+    if (json == null) {
+      debugWarn('Does not exist.', prefix: storageKey);
+      completer.complete(null);
+    } else {
+      debugLog('Fetched: $json', prefix: storageKey);
+      if (T == String) {
+        completer.complete(json as T);
+      } else {
+        final value = serializers.deserialize(jsonDecode(json)) as T;
+        completer.complete(value);
+      }
+    }
+  }));
+  return completer.future;
+}
+
+Future<bool> removeStorageObject<T>({required StorageObject type, int? tabId}) {
+  final storageKey = _createStorageKey(type, tabId);
+  final completer = Completer<bool>();
+  final storageArea = _getStorageArea(type.persistance);
+  storageArea.remove([storageKey], allowInterop(() {
+    debugLog('Removed object.', prefix: storageKey);
+    completer.complete(true);
+  }));
+  return completer.future;
+}
+
+void interceptStorageChange<T>({
+  required Object storageObj,
+  required StorageObject expectedType,
+  required void Function(T? storageObj) changeHandler,
+  int? tabId,
+}) {
+  try {
+    final expectedStorageKey = _createStorageKey(expectedType, tabId);
+    final isExpected = hasProperty(storageObj, expectedStorageKey);
+    if (!isExpected) return;
+
+    final objProp = getProperty(storageObj, expectedStorageKey);
+    final json = getProperty(objProp, 'newValue') as String?;
+    T? decodedObj;
+    if (json == null || T == String) {
+      decodedObj = json as T?;
+    } else {
+      decodedObj = serializers.deserialize(jsonDecode(json)) as T?;
+    }
+    debugLog('Intercepted $expectedStorageKey change: $json');
+    return changeHandler(decodedObj);
+  } catch (error) {
+    debugError(
+        'Error intercepting storage object with type $expectedType: $error');
+  }
+}
+
+StorageArea _getStorageArea(Persistance persistance) {
+  switch (persistance) {
+    case Persistance.acrossSessions:
+      return chrome.storage.local;
+    case Persistance.sessionOnly:
+      return chrome.storage.session;
+  }
+}
+
+String _createStorageKey(StorageObject type, int? tabId) {
+  if (tabId == null) return type.name;
+  return '$tabId-${type.name}';
+}
diff --git a/dwds/debug_extension_mv3/web/utils.dart b/dwds/debug_extension_mv3/web/utils.dart
new file mode 100644
index 0000000..c75549e
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/utils.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@JS()
+library utils;
+
+import 'dart:async';
+import 'dart:js_util';
+
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+
+Future<Tab> createTab(String url, {bool inNewWindow = false}) async {
+  if (inNewWindow) {
+    final windowPromise = chrome.windows.create(
+      WindowInfo(focused: true, url: url),
+    );
+    final windowObj = await promiseToFuture<WindowObj>(windowPromise);
+    return windowObj.tabs.first;
+  }
+  final tabPromise = chrome.tabs.create(TabInfo(
+    active: true,
+    url: url,
+  ));
+  return promiseToFuture<Tab>(tabPromise);
+}
+
+Future<Tab?> getTab(int tabId) {
+  return promiseToFuture<Tab?>(chrome.tabs.get(tabId));
+}
+
+Future<Tab?> getActiveTab() async {
+  final query = QueryInfo(active: true, currentWindow: true);
+  final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
+  return tabs.isNotEmpty ? tabs.first : null;
+}
+
+bool? _isDevMode;
+
+bool isDevMode() {
+  if (_isDevMode != null) {
+    return _isDevMode!;
+  }
+  final extensionManifest = chrome.runtime.getManifest();
+  final extensionName = getProperty(extensionManifest, 'name') ?? '';
+  return extensionName.contains('DEV');
+}
diff --git a/dwds/debug_extension_mv3/web/web_api.dart b/dwds/debug_extension_mv3/web/web_api.dart
index 938a0a1..1676fed 100644
--- a/dwds/debug_extension_mv3/web/web_api.dart
+++ b/dwds/debug_extension_mv3/web/web_api.dart
@@ -1,18 +1,63 @@
 // Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+import 'dart:html';
 
 import 'package:js/js.dart';
+import 'dart:js_util' as js_util;
 
 @JS()
-external Console get console;
+// ignore: non_constant_identifier_names
+external Json get JSON;
 
 @JS()
 @anonymous
-class Console {
-  external void log(String header,
-      [String style1, String style2, String style3]);
+class Json {
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
+  external String stringify(o);
+}
 
-  external void warn(String header,
-      [String style1, String style2, String style3]);
+// Custom implementation of Fetch API until the Dart implementation supports
+// credentials. See https://github.com/dart-lang/http/issues/595.
+@JS('fetch')
+external Object _nativeJsFetch(String resourceUrl, FetchOptions options);
+
+Future<FetchResponse> fetchRequest(String resourceUrl) async {
+  try {
+    final options = FetchOptions(
+      method: 'GET',
+      credentials: 'include',
+    );
+    final response =
+        await promiseToFuture(_nativeJsFetch(resourceUrl, options));
+    final body =
+        await promiseToFuture(js_util.callMethod(response, 'text', []));
+    final ok = js_util.getProperty<bool>(response, 'ok');
+    final status = js_util.getProperty<int>(response, 'status');
+    return FetchResponse(status: status, ok: ok, body: body);
+  } catch (error) {
+    return FetchResponse(
+        status: 400, ok: false, body: 'Error fetching $resourceUrl: $error');
+  }
+}
+
+@JS()
+@anonymous
+class FetchOptions {
+  external factory FetchOptions({
+    required String method, // e.g., 'GET', 'POST'
+    required String credentials, // e.g., 'omit', 'same-origin', 'include'
+  });
+}
+
+class FetchResponse {
+  final int status;
+  final bool ok;
+  final String? body;
+
+  FetchResponse({
+    required this.status,
+    required this.ok,
+    required this.body,
+  });
 }
diff --git a/dwds/lib/dart_web_debug_service.dart b/dwds/lib/dart_web_debug_service.dart
index b650041..5dce5f3 100644
--- a/dwds/lib/dart_web_debug_service.dart
+++ b/dwds/lib/dart_web_debug_service.dart
@@ -84,11 +84,13 @@
     bool enableDevtoolsLaunch = true,
     DevtoolsLauncher? devtoolsLauncher,
     bool launchDevToolsInNewWindow = true,
-    SdkConfigurationProvider? sdkConfigurationProvider,
+    SdkConfigurationProvider sdkConfigurationProvider =
+        const DefaultSdkConfigurationProvider(),
     bool emitDebugEvents = true,
+    bool isInternalBuild = false,
+    bool isFlutterApp = false,
   }) async {
     globalLoadStrategy = loadStrategy;
-    sdkConfigurationProvider ??= DefaultSdkConfigurationProvider();
 
     DevTools? devTools;
     Future<String>? extensionUri;
@@ -127,6 +129,8 @@
       extensionUri: extensionUri,
       enableDevtoolsLaunch: enableDevtoolsLaunch,
       emitDebugEvents: emitDebugEvents,
+      isInternalBuild: isInternalBuild,
+      isFlutterApp: isFlutterApp,
     );
 
     final devHandler = DevHandler(
diff --git a/dwds/lib/data/README.md b/dwds/lib/data/README.md
new file mode 100644
index 0000000..955d4cc
--- /dev/null
+++ b/dwds/lib/data/README.md
@@ -0,0 +1,16 @@
+# How to generate data files:
+
+## Creating a new data file:
+
+1. Create a new file for your data type in the `/data` directory with the
+   `.dart` extension
+1. Create an abstract class for your data type (see existing files for examples)
+1. Add the new data type to `/data/serializers.dart` 4 Run:
+   `dart run build_runner build` from DWDS root (this will generate the
+   `.g.dart` file)
+
+## To update an existing data file:
+
+1. Make your changes
+1. Run: `dart run build_runner clean` from DWDS root
+1. Run: `dart run build_runner build` from DWDS root
diff --git a/dwds/lib/data/debug_info.dart b/dwds/lib/data/debug_info.dart
new file mode 100644
index 0000000..288d1e7
--- /dev/null
+++ b/dwds/lib/data/debug_info.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:built_value/built_value.dart';
+import 'package:built_value/serializer.dart';
+
+part 'debug_info.g.dart';
+
+abstract class DebugInfo implements Built<DebugInfo, DebugInfoBuilder> {
+  static Serializer<DebugInfo> get serializer => _$debugInfoSerializer;
+
+  factory DebugInfo([Function(DebugInfoBuilder) updates]) = _$DebugInfo;
+
+  DebugInfo._();
+
+  String? get appEntrypointPath;
+  String? get appId;
+  String? get appInstanceId;
+  String? get appOrigin;
+  String? get appUrl;
+  String? get dwdsVersion;
+  String? get extensionUrl;
+  bool? get isInternalBuild;
+  bool? get isFlutterApp;
+}
diff --git a/dwds/lib/data/debug_info.g.dart b/dwds/lib/data/debug_info.g.dart
new file mode 100644
index 0000000..3807db2
--- /dev/null
+++ b/dwds/lib/data/debug_info.g.dart
@@ -0,0 +1,323 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'debug_info.dart';
+
+// **************************************************************************
+// BuiltValueGenerator
+// **************************************************************************
+
+Serializer<DebugInfo> _$debugInfoSerializer = new _$DebugInfoSerializer();
+
+class _$DebugInfoSerializer implements StructuredSerializer<DebugInfo> {
+  @override
+  final Iterable<Type> types = const [DebugInfo, _$DebugInfo];
+  @override
+  final String wireName = 'DebugInfo';
+
+  @override
+  Iterable<Object?> serialize(Serializers serializers, DebugInfo object,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = <Object?>[];
+    Object? value;
+    value = object.appEntrypointPath;
+    if (value != null) {
+      result
+        ..add('appEntrypointPath')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.appId;
+    if (value != null) {
+      result
+        ..add('appId')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.appInstanceId;
+    if (value != null) {
+      result
+        ..add('appInstanceId')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.appOrigin;
+    if (value != null) {
+      result
+        ..add('appOrigin')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.appUrl;
+    if (value != null) {
+      result
+        ..add('appUrl')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.dwdsVersion;
+    if (value != null) {
+      result
+        ..add('dwdsVersion')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.extensionUrl;
+    if (value != null) {
+      result
+        ..add('extensionUrl')
+        ..add(serializers.serialize(value,
+            specifiedType: const FullType(String)));
+    }
+    value = object.isInternalBuild;
+    if (value != null) {
+      result
+        ..add('isInternalBuild')
+        ..add(
+            serializers.serialize(value, specifiedType: const FullType(bool)));
+    }
+    value = object.isFlutterApp;
+    if (value != null) {
+      result
+        ..add('isFlutterApp')
+        ..add(
+            serializers.serialize(value, specifiedType: const FullType(bool)));
+    }
+    return result;
+  }
+
+  @override
+  DebugInfo deserialize(Serializers serializers, Iterable<Object?> serialized,
+      {FullType specifiedType = FullType.unspecified}) {
+    final result = new DebugInfoBuilder();
+
+    final iterator = serialized.iterator;
+    while (iterator.moveNext()) {
+      final key = iterator.current! as String;
+      iterator.moveNext();
+      final Object? value = iterator.current;
+      switch (key) {
+        case 'appEntrypointPath':
+          result.appEntrypointPath = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'appId':
+          result.appId = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'appInstanceId':
+          result.appInstanceId = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'appOrigin':
+          result.appOrigin = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'appUrl':
+          result.appUrl = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'dwdsVersion':
+          result.dwdsVersion = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'extensionUrl':
+          result.extensionUrl = serializers.deserialize(value,
+              specifiedType: const FullType(String)) as String?;
+          break;
+        case 'isInternalBuild':
+          result.isInternalBuild = serializers.deserialize(value,
+              specifiedType: const FullType(bool)) as bool?;
+          break;
+        case 'isFlutterApp':
+          result.isFlutterApp = serializers.deserialize(value,
+              specifiedType: const FullType(bool)) as bool?;
+          break;
+      }
+    }
+
+    return result.build();
+  }
+}
+
+class _$DebugInfo extends DebugInfo {
+  @override
+  final String? appEntrypointPath;
+  @override
+  final String? appId;
+  @override
+  final String? appInstanceId;
+  @override
+  final String? appOrigin;
+  @override
+  final String? appUrl;
+  @override
+  final String? dwdsVersion;
+  @override
+  final String? extensionUrl;
+  @override
+  final bool? isInternalBuild;
+  @override
+  final bool? isFlutterApp;
+
+  factory _$DebugInfo([void Function(DebugInfoBuilder)? updates]) =>
+      (new DebugInfoBuilder()..update(updates))._build();
+
+  _$DebugInfo._(
+      {this.appEntrypointPath,
+      this.appId,
+      this.appInstanceId,
+      this.appOrigin,
+      this.appUrl,
+      this.dwdsVersion,
+      this.extensionUrl,
+      this.isInternalBuild,
+      this.isFlutterApp})
+      : super._();
+
+  @override
+  DebugInfo rebuild(void Function(DebugInfoBuilder) updates) =>
+      (toBuilder()..update(updates)).build();
+
+  @override
+  DebugInfoBuilder toBuilder() => new DebugInfoBuilder()..replace(this);
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) return true;
+    return other is DebugInfo &&
+        appEntrypointPath == other.appEntrypointPath &&
+        appId == other.appId &&
+        appInstanceId == other.appInstanceId &&
+        appOrigin == other.appOrigin &&
+        appUrl == other.appUrl &&
+        dwdsVersion == other.dwdsVersion &&
+        extensionUrl == other.extensionUrl &&
+        isInternalBuild == other.isInternalBuild &&
+        isFlutterApp == other.isFlutterApp;
+  }
+
+  @override
+  int get hashCode {
+    var _$hash = 0;
+    _$hash = $jc(_$hash, appEntrypointPath.hashCode);
+    _$hash = $jc(_$hash, appId.hashCode);
+    _$hash = $jc(_$hash, appInstanceId.hashCode);
+    _$hash = $jc(_$hash, appOrigin.hashCode);
+    _$hash = $jc(_$hash, appUrl.hashCode);
+    _$hash = $jc(_$hash, dwdsVersion.hashCode);
+    _$hash = $jc(_$hash, extensionUrl.hashCode);
+    _$hash = $jc(_$hash, isInternalBuild.hashCode);
+    _$hash = $jc(_$hash, isFlutterApp.hashCode);
+    _$hash = $jf(_$hash);
+    return _$hash;
+  }
+
+  @override
+  String toString() {
+    return (newBuiltValueToStringHelper(r'DebugInfo')
+          ..add('appEntrypointPath', appEntrypointPath)
+          ..add('appId', appId)
+          ..add('appInstanceId', appInstanceId)
+          ..add('appOrigin', appOrigin)
+          ..add('appUrl', appUrl)
+          ..add('dwdsVersion', dwdsVersion)
+          ..add('extensionUrl', extensionUrl)
+          ..add('isInternalBuild', isInternalBuild)
+          ..add('isFlutterApp', isFlutterApp))
+        .toString();
+  }
+}
+
+class DebugInfoBuilder implements Builder<DebugInfo, DebugInfoBuilder> {
+  _$DebugInfo? _$v;
+
+  String? _appEntrypointPath;
+  String? get appEntrypointPath => _$this._appEntrypointPath;
+  set appEntrypointPath(String? appEntrypointPath) =>
+      _$this._appEntrypointPath = appEntrypointPath;
+
+  String? _appId;
+  String? get appId => _$this._appId;
+  set appId(String? appId) => _$this._appId = appId;
+
+  String? _appInstanceId;
+  String? get appInstanceId => _$this._appInstanceId;
+  set appInstanceId(String? appInstanceId) =>
+      _$this._appInstanceId = appInstanceId;
+
+  String? _appOrigin;
+  String? get appOrigin => _$this._appOrigin;
+  set appOrigin(String? appOrigin) => _$this._appOrigin = appOrigin;
+
+  String? _appUrl;
+  String? get appUrl => _$this._appUrl;
+  set appUrl(String? appUrl) => _$this._appUrl = appUrl;
+
+  String? _dwdsVersion;
+  String? get dwdsVersion => _$this._dwdsVersion;
+  set dwdsVersion(String? dwdsVersion) => _$this._dwdsVersion = dwdsVersion;
+
+  String? _extensionUrl;
+  String? get extensionUrl => _$this._extensionUrl;
+  set extensionUrl(String? extensionUrl) => _$this._extensionUrl = extensionUrl;
+
+  bool? _isInternalBuild;
+  bool? get isInternalBuild => _$this._isInternalBuild;
+  set isInternalBuild(bool? isInternalBuild) =>
+      _$this._isInternalBuild = isInternalBuild;
+
+  bool? _isFlutterApp;
+  bool? get isFlutterApp => _$this._isFlutterApp;
+  set isFlutterApp(bool? isFlutterApp) => _$this._isFlutterApp = isFlutterApp;
+
+  DebugInfoBuilder();
+
+  DebugInfoBuilder get _$this {
+    final $v = _$v;
+    if ($v != null) {
+      _appEntrypointPath = $v.appEntrypointPath;
+      _appId = $v.appId;
+      _appInstanceId = $v.appInstanceId;
+      _appOrigin = $v.appOrigin;
+      _appUrl = $v.appUrl;
+      _dwdsVersion = $v.dwdsVersion;
+      _extensionUrl = $v.extensionUrl;
+      _isInternalBuild = $v.isInternalBuild;
+      _isFlutterApp = $v.isFlutterApp;
+      _$v = null;
+    }
+    return this;
+  }
+
+  @override
+  void replace(DebugInfo other) {
+    ArgumentError.checkNotNull(other, 'other');
+    _$v = other as _$DebugInfo;
+  }
+
+  @override
+  void update(void Function(DebugInfoBuilder)? updates) {
+    if (updates != null) updates(this);
+  }
+
+  @override
+  DebugInfo build() => _build();
+
+  _$DebugInfo _build() {
+    final _$result = _$v ??
+        new _$DebugInfo._(
+            appEntrypointPath: appEntrypointPath,
+            appId: appId,
+            appInstanceId: appInstanceId,
+            appOrigin: appOrigin,
+            appUrl: appUrl,
+            dwdsVersion: dwdsVersion,
+            extensionUrl: extensionUrl,
+            isInternalBuild: isInternalBuild,
+            isFlutterApp: isFlutterApp);
+    replace(_$result);
+    return _$result;
+  }
+}
+
+// ignore_for_file: deprecated_member_use_from_same_package,type=lint
diff --git a/dwds/lib/data/serializers.dart b/dwds/lib/data/serializers.dart
index 903ef59..25f4e2a 100644
--- a/dwds/lib/data/serializers.dart
+++ b/dwds/lib/data/serializers.dart
@@ -8,6 +8,7 @@
 import 'build_result.dart';
 import 'connect_request.dart';
 import 'debug_event.dart';
+import 'debug_info.dart';
 import 'devtools_request.dart';
 import 'error_response.dart';
 import 'extension_request.dart';
@@ -24,6 +25,7 @@
   BuildResult,
   ConnectRequest,
   DebugEvent,
+  DebugInfo,
   DevToolsRequest,
   DevToolsResponse,
   IsolateExit,
diff --git a/dwds/lib/data/serializers.g.dart b/dwds/lib/data/serializers.g.dart
index 6810d86..584ce7e 100644
--- a/dwds/lib/data/serializers.g.dart
+++ b/dwds/lib/data/serializers.g.dart
@@ -13,6 +13,7 @@
       ..add(BuildStatus.serializer)
       ..add(ConnectRequest.serializer)
       ..add(DebugEvent.serializer)
+      ..add(DebugInfo.serializer)
       ..add(DevToolsRequest.serializer)
       ..add(DevToolsResponse.serializer)
       ..add(ErrorResponse.serializer)
diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart
index 84bf219..3b67f1a 100644
--- a/dwds/lib/dwds.dart
+++ b/dwds/lib/dwds.dart
@@ -29,4 +29,4 @@
 export 'src/services/expression_compiler_service.dart'
     show ExpressionCompilerService;
 export 'src/utilities/sdk_configuration.dart'
-    show SdkConfiguration, SdkConfigurationProvider;
+    show SdkLayout, SdkConfiguration, SdkConfigurationProvider;
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart
index 4f56293..0f3f754 100644
--- a/dwds/lib/src/debugging/debugger.dart
+++ b/dwds/lib/src/debugging/debugger.dart
@@ -5,8 +5,8 @@
 import 'dart:async';
 import 'dart:math' as math;
 
+import 'package:dwds/src/utilities/synchronized.dart';
 import 'package:logging/logging.dart';
-import 'package:pool/pool.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
     hide StackTrace;
@@ -373,7 +373,7 @@
     // Filter out variables that do not come from dart code, such as native
     // JavaScript objects
     return boundVariables
-        .where((bv) => bv != null && !isNativeJsObject(bv.value as InstanceRef))
+        .where((bv) => isDisplayableObject(bv?.value))
         .toList()
         .cast();
   }
@@ -744,6 +744,9 @@
   return result;
 }
 
+bool isDisplayableObject(Object? object) =>
+    object is Sentinel || object is InstanceRef && !isNativeJsObject(object);
+
 bool isNativeJsObject(InstanceRef instanceRef) {
   // New type representation of JS objects reifies them to a type suffixed with
   // JavaScriptObject.
@@ -776,7 +779,7 @@
 
   final _bpByDartId = <String, Future<Breakpoint>>{};
 
-  final _pool = Pool(1);
+  final _queue = AtomicQueue();
 
   final Locations locations;
   final RemoteDebugger remoteDebugger;
@@ -865,7 +868,7 @@
     final urlRegex = '.*${location.jsLocation.module}.*';
     // Prevent `Aww, snap!` errors when setting multiple breakpoints
     // simultaneously by serializing the requests.
-    return _pool.withResource(() async {
+    return _queue.run(() async {
       final breakPointId = await sendCommandAndValidateResult<String>(
           remoteDebugger,
           method: 'Debugger.setBreakpointByUrl',
diff --git a/dwds/lib/src/debugging/frame_computer.dart b/dwds/lib/src/debugging/frame_computer.dart
index 22573a9..00d7ca2 100644
--- a/dwds/lib/src/debugging/frame_computer.dart
+++ b/dwds/lib/src/debugging/frame_computer.dart
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'package:pool/pool.dart';
+import 'package:dwds/src/utilities/synchronized.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
@@ -11,9 +11,9 @@
 class FrameComputer {
   final Debugger debugger;
 
-  // To ensure that the frames are computed only once, we use a pool to guard
-  // the work. Frames are computed sequentially.
-  final _pool = Pool(1);
+  // To ensure that the frames are computed only once, we use an atomic queue
+  // to guard the work. Frames are computed sequentially.
+  final _queue = AtomicQueue();
 
   final List<WipCallFrame> _callFrames;
   final List<Frame> _computedFrames = [];
@@ -36,7 +36,7 @@
   /// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
   /// [Frame]s.
   Future<List<Frame>> calculateFrames({int? limit}) async {
-    return _pool.withResource(() async {
+    return _queue.run(() async {
       if (limit != null && _computedFrames.length >= limit) {
         return _computedFrames.take(limit).toList();
       }
diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart
index 3b1e22b..083a0ef 100644
--- a/dwds/lib/src/debugging/inspector.dart
+++ b/dwds/lib/src/debugging/inspector.dart
@@ -83,6 +83,13 @@
   /// Regex used to extract the message from an exception description.
   static final exceptionMessageRegex = RegExp(r'^.*$', multiLine: true);
 
+  /// Flutter widget inspector library.
+  Future<LibraryRef?> get flutterWidgetInspectorLibrary => _libraryHelper
+      .libraryRefFor('package:flutter/src/widgets/widget_inspector.dart');
+
+  /// Regex used to extract a stack trace line from the exception description.
+  static final stackTraceLineRegex = RegExp(r'^\s*at\s.*$', multiLine: true);
+
   AppInspector._(
     this._appConnection,
     this._isolate,
@@ -611,8 +618,17 @@
     if (mappedStack == null || mappedStack.isEmpty) {
       return description;
     }
-    var message = exceptionMessageRegex.firstMatch(description)?.group(0);
-    message = (message != null) ? '$message\n' : '';
+    final message = _allLinesBeforeStackTrace(description);
     return '$message$mappedStack';
   }
+
+  String _allLinesBeforeStackTrace(String description) {
+    var message = '';
+    for (final match in exceptionMessageRegex.allMatches(description)) {
+      final isStackTraceLine = stackTraceLineRegex.hasMatch(match[0] ?? '');
+      if (isStackTraceLine) break;
+      message += '${match[0]}\n';
+    }
+    return message;
+  }
 }
diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart
index 03851d9..4f41d91 100644
--- a/dwds/lib/src/debugging/instance.dart
+++ b/dwds/lib/src/debugging/instance.dart
@@ -180,7 +180,7 @@
     var boundFields = await Future.wait(
         dartProperties.map<Future<BoundField>>((p) => _fieldFor(p, classRef)));
     boundFields = boundFields
-        .where((bv) => !isNativeJsObject(bv.value as InstanceRef))
+        .where((bv) => isDisplayableObject(bv.value))
         .toList()
       ..sort(_compareBoundFields);
     final result = Instance(
diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart
index cb7c5b7..4ead6d7 100644
--- a/dwds/lib/src/dwds_vm_client.dart
+++ b/dwds/lib/src/dwds_vm_client.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:dwds/src/utilities/synchronized.dart';
 import 'package:logging/logging.dart';
 import 'package:uuid/uuid.dart';
 import 'package:vm_service/vm_service.dart';
@@ -32,6 +33,9 @@
   /// All subsequent calls to [close] will return this future.
   Future<void>? _closed;
 
+  /// Synchronizes hot restarts to avoid races.
+  final _hotRestartQueue = AtomicQueue();
+
   DwdsVmClient(this.client, this._requestController, this._responseController);
 
   Future<void> close() => _closed ??= () async {
@@ -60,6 +64,9 @@
     final chromeProxyService =
         debugService.chromeProxyService as ChromeProxyService;
 
+    final dwdsVmClient =
+        DwdsVmClient(client, requestController, responseController);
+
     // Register '_flutter.listViews' method on the chrome proxy service vm.
     // In native world, this method is provided by the engine, but the web
     // engine is not aware of the VM uri or the isolates.
@@ -85,7 +92,7 @@
     client.registerServiceCallback(
         'hotRestart',
         (request) => captureElapsedTime(
-            () => _hotRestart(chromeProxyService, client),
+            () => dwdsVmClient.hotRestart(chromeProxyService, client),
             (_) => DwdsEvent.hotRestart()));
     await client.registerService('hotRestart', 'DWDS');
 
@@ -131,14 +138,19 @@
               'error': {
                 'code': kFeatureDisabled,
                 'message': kFeatureDisabledMessage,
-                'details':
+                'data':
                     'Existing VM service clients prevent DDS from taking control.',
               },
             };
     });
     await client.registerService('_yieldControlToDDS', 'DWDS');
 
-    return DwdsVmClient(client, requestController, responseController);
+    return dwdsVmClient;
+  }
+
+  Future<Map<String, dynamic>> hotRestart(
+      ChromeProxyService chromeProxyService, VmService client) async {
+    return _hotRestartQueue.run(() => _hotRestart(chromeProxyService, client));
   }
 }
 
@@ -153,22 +165,7 @@
         final action = payload?['action'] as String?;
         final screen = payload?['screen'] as String?;
         if (screen != null && action == 'pageReady') {
-          if (dwdsStats.isFirstDebuggerReady) {
-            final debuggerReadyTime = DateTime.now()
-                .difference(dwdsStats.devToolsStart)
-                .inMilliseconds;
-            emitEvent(DwdsEvent.devToolsLoad(debuggerReadyTime, screen));
-            _logger.fine('DevTools load time: $debuggerReadyTime ms');
-            final debuggerStartTime = DateTime.now()
-                .difference(dwdsStats.debuggerStart)
-                .inMilliseconds;
-            emitEvent(DwdsEvent.debuggerReady(debuggerStartTime, screen));
-            _logger.fine('Debugger ready time: $debuggerStartTime ms');
-          } else {
-            _logger
-                .finest('Debugger and DevTools startup times already recorded.'
-                    ' Ignoring $event.');
-          }
+          _recordDwdsStats(dwdsStats, screen);
         } else {
           _logger.finest('Ignoring unknown event: $event');
         }
@@ -176,6 +173,27 @@
   }
 }
 
+void _recordDwdsStats(DwdsStats dwdsStats, String screen) {
+  if (dwdsStats.isFirstDebuggerReady) {
+    final devToolsStart = dwdsStats.devToolsStart;
+    final debuggerStart = dwdsStats.debuggerStart;
+    if (devToolsStart != null) {
+      final devToolLoadTime =
+          DateTime.now().difference(devToolsStart).inMilliseconds;
+      emitEvent(DwdsEvent.devToolsLoad(devToolLoadTime, screen));
+      _logger.fine('DevTools load time: $devToolLoadTime ms');
+    }
+    if (debuggerStart != null) {
+      final debuggerReadyTime =
+          DateTime.now().difference(debuggerStart).inMilliseconds;
+      emitEvent(DwdsEvent.debuggerReady(debuggerReadyTime, screen));
+      _logger.fine('Debugger ready time: $debuggerReadyTime ms');
+    }
+  } else {
+    _logger.finest('Debugger and DevTools stats are already recorded.');
+  }
+}
+
 Future<Map<String, dynamic>> _hotRestart(
     ChromeProxyService chromeProxyService, VmService client) async {
   _logger.info('Attempting a hot restart');
diff --git a/dwds/lib/src/events.dart b/dwds/lib/src/events.dart
index 9cdb13b..8634a62 100644
--- a/dwds/lib/src/events.dart
+++ b/dwds/lib/src/events.dart
@@ -8,12 +8,12 @@
 
 class DwdsStats {
   /// The time when the user starts the debugger.
-  late DateTime _debuggerStart;
-  DateTime get debuggerStart => _debuggerStart;
+  DateTime? _debuggerStart;
+  DateTime? get debuggerStart => _debuggerStart;
 
   /// The time when dwds launches DevTools.
-  late DateTime _devToolsStart;
-  DateTime get devToolsStart => _devToolsStart;
+  DateTime? _devToolsStart;
+  DateTime? get devToolsStart => _devToolsStart;
 
   /// Records and returns weither the debugger is ready.
   bool _isFirstDebuggerReady = true;
diff --git a/dwds/lib/src/handlers/injector.dart b/dwds/lib/src/handlers/injector.dart
index 9ad89cb..b486cc0 100644
--- a/dwds/lib/src/handlers/injector.dart
+++ b/dwds/lib/src/handlers/injector.dart
@@ -36,6 +36,8 @@
   final bool _enableDevtoolsLaunch;
   final bool _useSseForInjectedClient;
   final bool _emitDebugEvents;
+  final bool _isInternalBuild;
+  final bool _isFlutterApp;
 
   DwdsInjector(
     this._loadStrategy, {
@@ -43,10 +45,14 @@
     bool enableDevtoolsLaunch = false,
     bool useSseForInjectedClient = true,
     bool emitDebugEvents = true,
+    bool isInternalBuild = false,
+    bool isFlutterApp = false,
   })  : _extensionUri = extensionUri,
         _enableDevtoolsLaunch = enableDevtoolsLaunch,
         _useSseForInjectedClient = useSseForInjectedClient,
-        _emitDebugEvents = emitDebugEvents;
+        _emitDebugEvents = emitDebugEvents,
+        _isInternalBuild = isInternalBuild,
+        _isFlutterApp = isFlutterApp;
 
   /// Returns the embedded dev handler paths.
   ///
@@ -111,6 +117,8 @@
                 _loadStrategy,
                 _enableDevtoolsLaunch,
                 _emitDebugEvents,
+                _isInternalBuild,
+                _isFlutterApp,
               );
               body += await _loadStrategy.bootstrapFor(entrypoint);
               _logger.info('Injected debugging metadata for '
@@ -136,15 +144,16 @@
 /// Returns the provided body with the main function hoisted into a global
 /// variable and a snippet of JS that loads the injected client.
 String _injectClientAndHoistMain(
-  String body,
-  String appId,
-  String devHandlerPath,
-  String entrypointPath,
-  String? extensionUri,
-  LoadStrategy loadStrategy,
-  bool enableDevtoolsLaunch,
-  bool emitDebugEvents,
-) {
+    String body,
+    String appId,
+    String devHandlerPath,
+    String entrypointPath,
+    String? extensionUri,
+    LoadStrategy loadStrategy,
+    bool enableDevtoolsLaunch,
+    bool emitDebugEvents,
+    bool isInternalBuild,
+    bool isFlutterApp) {
   final bodyLines = body.split('\n');
   final extensionIndex =
       bodyLines.indexWhere((line) => line.contains(mainExtensionMarker));
@@ -157,14 +166,15 @@
   // application to be in a ready state, that is the main function is hoisted
   // and the Dart SDK is loaded.
   final injectedClientSnippet = _injectedClientSnippet(
-    appId,
-    devHandlerPath,
-    entrypointPath,
-    extensionUri,
-    loadStrategy,
-    enableDevtoolsLaunch,
-    emitDebugEvents,
-  );
+      appId,
+      devHandlerPath,
+      entrypointPath,
+      extensionUri,
+      loadStrategy,
+      enableDevtoolsLaunch,
+      emitDebugEvents,
+      isInternalBuild,
+      isFlutterApp);
   result += '''
   // Injected by dwds for debugging support.
   if(!window.\$dwdsInitialized) {
@@ -198,6 +208,8 @@
   LoadStrategy loadStrategy,
   bool enableDevtoolsLaunch,
   bool emitDebugEvents,
+  bool isInternalBuild,
+  bool isFlutterApp,
 ) {
   var injectedBody = 'window.\$dartAppId = "$appId";\n'
       'window.\$dartReloadConfiguration = "${loadStrategy.reloadConfiguration}";\n'
@@ -208,6 +220,8 @@
       'window.\$dwdsEnableDevtoolsLaunch = $enableDevtoolsLaunch;\n'
       'window.\$dartEntrypointPath = "$entrypointPath";\n'
       'window.\$dartEmitDebugEvents = $emitDebugEvents;\n'
+      'window.\$isInternalBuild = $isInternalBuild;\n'
+      'window.\$isFlutterApp = $isFlutterApp;\n'
       '${loadStrategy.loadClientSnippet(_clientScript)}';
   if (extensionUri != null) {
     injectedBody += 'window.\$dartExtensionUri = "$extensionUri";\n';
diff --git a/dwds/lib/src/services/batched_expression_evaluator.dart b/dwds/lib/src/services/batched_expression_evaluator.dart
index 64338fd..81bf7f0 100644
--- a/dwds/lib/src/services/batched_expression_evaluator.dart
+++ b/dwds/lib/src/services/batched_expression_evaluator.dart
@@ -31,6 +31,7 @@
   final Debugger _debugger;
   final _requestController =
       BatchedStreamController<EvaluateRequest>(delay: 200);
+  bool _closed = false;
 
   BatchedExpressionEvaluator(
     String entrypoint,
@@ -45,8 +46,10 @@
 
   @override
   void close() {
+    if (_closed) return;
     _logger.fine('Closed');
     _requestController.close();
+    _closed = true;
   }
 
   @override
@@ -55,7 +58,11 @@
     String? libraryUri,
     String expression,
     Map<String, String>? scope,
-  ) {
+  ) async {
+    if (_closed) {
+      return createError(
+          ErrorKind.internal, 'Batched expression evaluator closed');
+    }
     final request = EvaluateRequest(isolateId, libraryUri, expression, scope);
     _requestController.sink.add(request);
     return request.completer.future;
@@ -121,15 +128,22 @@
       final request = requests[i];
       if (request.completer.isCompleted) continue;
       _logger.fine('Getting result out of a batch for ${request.expression}');
-      _debugger
-          .getProperties(list.objectId!,
-              offset: i, count: 1, length: requests.length)
-          .then((v) {
-        final result = v.first.value;
-        _logger.fine(
-            'Got result out of a batch for ${request.expression}: $result');
-        request.completer.complete(result);
-      });
+
+      final listId = list.objectId;
+      if (listId == null) {
+        final error =
+            createError(ErrorKind.internal, 'No batch result object ID.');
+        request.completer.complete(error);
+      } else {
+        unawaited(_debugger
+            .getProperties(listId, offset: i, count: 1, length: requests.length)
+            .then((v) {
+          final result = v.first.value;
+          _logger.fine(
+              'Got result out of a batch for ${request.expression}: $result');
+          request.completer.complete(result);
+        }));
+      }
     }
   }
 }
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 6325cfa..766f3e9 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -203,6 +203,34 @@
     }
   }
 
+  Future<void> _prewarmExpressionCompilerCache() async {
+    // Exit early if the expression evaluation is not enabled.
+    if (_compiler == null || _expressionEvaluator == null) {
+      return;
+    }
+    // Wait until the inspector is ready.
+    await isInitialized;
+
+    // Pre-warm the flutter framework module cache in the compiler.
+    //
+    // Flutter inspector relies on evaluations in widget_inspector
+    // library, which is a part of the flutter framework module, to
+    // produce widget trees, draw the layout explorer, show hover
+    // cards etc.
+    // Pre-warming the cache while DevTools is still loading helps
+    // Flutter Inspector start faster.
+    final libraryToCache = await inspector.flutterWidgetInspectorLibrary;
+    if (libraryToCache != null) {
+      final isolateId = inspector.isolateRef.id;
+      final libraryId = libraryToCache.id;
+      if (isolateId != null && libraryId != null) {
+        _logger.finest(
+            'Caching ${libraryToCache.uri} in expression compiler worker');
+        await evaluate(isolateId, libraryId, 'true');
+      }
+    }
+  }
+
   /// Creates a new isolate.
   ///
   /// Only one isolate at a time is supported, but they should be cleaned up
@@ -251,19 +279,15 @@
             compiler,
           );
 
+    unawaited(_prewarmExpressionCompilerCache());
+
     await debugger.reestablishBreakpoints(
         _previousBreakpoints, _disabledBreakpoints);
     _disabledBreakpoints.clear();
 
     unawaited(appConnection.onStart.then((_) async {
       await debugger.resumeFromStart();
-      if (!_startedCompleter.isCompleted) {
-        _startedCompleter.complete();
-      } else {
-        // See https://github.com/flutter/flutter/issues/117676:
-        _logger
-            .warning('Unexpected state, debugging may not work as expected.');
-      }
+      _startedCompleter.complete();
     }));
 
     unawaited(appConnection.onDone.then((_) => destroyIsolate()));
@@ -312,6 +336,7 @@
   void destroyIsolate() {
     _logger.fine('Destroying isolate');
     if (!_isIsolateRunning) return;
+
     final isolate = inspector.isolate;
     final isolateRef = inspector.isolateRef;
 
@@ -419,10 +444,20 @@
     return _rpcNotSupportedFuture('clearVMTimeline');
   }
 
-  Future<Response> _getEvaluationResult(
+  Future<Response> _getEvaluationResult(String isolateId,
       Future<RemoteObject> Function() evaluation, String expression) async {
     try {
       final result = await evaluation();
+      if (!_isIsolateRunning || isolateId != inspector.isolate.id) {
+        _logger.fine('Cannot get evaluation result for isolate $isolateId: '
+            ' isolate exited.');
+        return ErrorRef(
+          kind: 'error',
+          message: 'Isolate exited',
+          id: createId(),
+        );
+      }
+
       // Handle compilation errors, internal errors,
       // and reference errors from JavaScript evaluation in chrome.
       if (result.type.contains('Error')) {
@@ -473,6 +508,7 @@
 
         final library = await inspector.getLibrary(targetId);
         return await _getEvaluationResult(
+            isolateId,
             () => evaluator.evaluateExpression(
                 isolateId, library?.uri, expression, scope),
             expression);
@@ -496,6 +532,7 @@
         _checkIsolate('evaluateInFrame', isolateId);
 
         return await _getEvaluationResult(
+            isolateId,
             () => evaluator.evaluateExpressionInFrame(
                 isolateId, frameIndex, expression, scope),
             expression);
@@ -752,20 +789,25 @@
     }
   }
 
+  /// This method is deprecated in vm_service package.
+  ///
+  /// TODO(annagrin): remove after dart-code and IntelliJ stop using this API.
+  /// Issue: https://github.com/dart-lang/webdev/issues/1868
+  ///
+  // ignore: annotate_overrides
+  Future<Success> setExceptionPauseMode(
+          String isolateId, /*ExceptionPauseMode*/ String mode) =>
+      setIsolatePauseMode(isolateId, exceptionPauseMode: mode);
+
   @override
   Future<Success> setIsolatePauseMode(String isolateId,
       {String? exceptionPauseMode, bool? shouldPauseOnExit}) async {
     // TODO(elliette): Is there a way to respect the shouldPauseOnExit parameter
     // in Chrome?
-    return setExceptionPauseMode(
-        isolateId, exceptionPauseMode ?? ExceptionPauseMode.kNone);
-  }
-
-  @override
-  Future<Success> setExceptionPauseMode(String isolateId, String mode) async {
     await isInitialized;
-    _checkIsolate('setExceptionPauseMode', isolateId);
-    return (await debuggerFuture).setExceptionPauseMode(mode);
+    _checkIsolate('setIsolatePauseMode', isolateId);
+    return (await debuggerFuture)
+        .setExceptionPauseMode(exceptionPauseMode ?? ExceptionPauseMode.kNone);
   }
 
   @override
diff --git a/dwds/lib/src/services/expression_compiler_service.dart b/dwds/lib/src/services/expression_compiler_service.dart
index 8d738fe..e94be84 100644
--- a/dwds/lib/src/services/expression_compiler_service.dart
+++ b/dwds/lib/src/services/expression_compiler_service.dart
@@ -68,15 +68,21 @@
     String moduleFormat,
     bool soundNullSafety,
     SdkConfiguration sdkConfiguration,
+    List<String> experiments,
     bool verbose,
   ) async {
-    sdkConfiguration.validate();
+    sdkConfiguration.validateSdkDir();
+    if (soundNullSafety) {
+      sdkConfiguration.validateSoundSummaries();
+    } else {
+      sdkConfiguration.validateWeakSummaries();
+    }
 
     final librariesUri = sdkConfiguration.librariesUri!;
     final workerUri = sdkConfiguration.compilerWorkerUri!;
     final sdkSummaryUri = soundNullSafety
         ? sdkConfiguration.soundSdkSummaryUri!
-        : sdkConfiguration.unsoundSdkSummaryUri!;
+        : sdkConfiguration.weakSdkSummaryUri!;
 
     final args = [
       '--experimental-expression-compiler',
@@ -92,6 +98,7 @@
       moduleFormat,
       if (verbose) '--verbose',
       soundNullSafety ? '--sound-null-safety' : '--no-sound-null-safety',
+      for (var experiment in experiments) '--enable-experiment=$experiment',
     ];
 
     _logger.info('Starting...');
@@ -230,16 +237,20 @@
   final _compiler = Completer<_Compiler>();
   final String _address;
   final FutureOr<int> _port;
+  final List<String> experiments;
   final bool _verbose;
 
   final SdkConfigurationProvider _sdkConfigurationProvider;
 
-  ExpressionCompilerService(this._address, this._port,
-      {bool verbose = false,
-      SdkConfigurationProvider? sdkConfigurationProvider})
-      : _verbose = verbose,
-        _sdkConfigurationProvider =
-            sdkConfigurationProvider ?? DefaultSdkConfigurationProvider();
+  ExpressionCompilerService(
+    this._address,
+    this._port, {
+    bool verbose = false,
+    SdkConfigurationProvider sdkConfigurationProvider =
+        const DefaultSdkConfigurationProvider(),
+    this.experiments = const [],
+  })  : _verbose = verbose,
+        _sdkConfigurationProvider = sdkConfigurationProvider;
 
   @override
   Future<ExpressionCompilationResult> compileExpressionToJs(
@@ -265,6 +276,7 @@
       moduleFormat,
       soundNullSafety,
       await _sdkConfigurationProvider.configuration,
+      experiments,
       _verbose,
     );
 
diff --git a/dwds/lib/src/services/expression_evaluator.dart b/dwds/lib/src/services/expression_evaluator.dart
index 35aef1c..e932775 100644
--- a/dwds/lib/src/services/expression_evaluator.dart
+++ b/dwds/lib/src/services/expression_evaluator.dart
@@ -2,8 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:async';
-
 import 'package:dwds/src/utilities/domain.dart';
 import 'package:logging/logging.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
@@ -43,6 +41,7 @@
   final Modules _modules;
   final ExpressionCompiler _compiler;
   final _logger = Logger('ExpressionEvaluator');
+  bool _closed = false;
 
   /// Strip synthetic library name from compiler error messages.
   static final _syntheticNameFilterRegex =
@@ -58,12 +57,14 @@
   ExpressionEvaluator(this._entrypoint, this._inspector, this._debugger,
       this._locations, this._modules, this._compiler);
 
-  RemoteObject _createError(ErrorKind severity, String message) {
+  RemoteObject createError(ErrorKind severity, String message) {
     return RemoteObject(
         <String, String>{'type': '$severity', 'value': message});
   }
 
-  void close() {}
+  void close() {
+    _closed = true;
+  }
 
   /// Evaluate dart expression inside a given library.
   ///
@@ -82,19 +83,23 @@
     String expression,
     Map<String, String>? scope,
   ) async {
+    if (_closed) {
+      return createError(ErrorKind.internal, 'expression evaluator closed.');
+    }
+
     scope ??= {};
 
     if (expression.isEmpty) {
-      return _createError(ErrorKind.invalidInput, expression);
+      return createError(ErrorKind.invalidInput, expression);
     }
 
     if (libraryUri == null) {
-      return _createError(ErrorKind.invalidInput, 'no library uri');
+      return createError(ErrorKind.invalidInput, 'no library uri');
     }
 
     final module = await _modules.moduleForLibrary(libraryUri);
     if (module == null) {
-      return _createError(ErrorKind.internal, 'no module for $libraryUri');
+      return createError(ErrorKind.internal, 'no module for $libraryUri');
     }
 
     // Wrap the expression in a lambda so we can call it as a function.
@@ -120,7 +125,8 @@
     var result = await _inspector.callFunction(jsCode, scope.values);
     result = await _formatEvaluationError(result);
 
-    _logger.finest('Evaluated "$expression" to "$result"');
+    _logger
+        .finest('Evaluated "$expression" to "$result" for isolate $isolateId');
     return result;
   }
 
@@ -141,20 +147,20 @@
     if (scope != null) {
       // TODO(annagrin): Implement scope support.
       // Issue: https://github.com/dart-lang/webdev/issues/1344
-      return _createError(
+      return createError(
           ErrorKind.internal,
           'Using scope for expression evaluation in frame '
           'is not supported.');
     }
 
     if (expression.isEmpty) {
-      return _createError(ErrorKind.invalidInput, expression);
+      return createError(ErrorKind.invalidInput, expression);
     }
 
     // Get JS scope and current JS location.
     final jsFrame = _debugger.jsFrameForIndex(frameIndex);
     if (jsFrame == null) {
-      return _createError(
+      return createError(
           ErrorKind.internal,
           'Expression evaluation in async frames '
           'is not supported. No frame with index $frameIndex.');
@@ -169,12 +175,12 @@
     // Find corresponding dart location and scope.
     final url = _debugger.urlForScriptId(jsScriptId);
     if (url == null) {
-      return _createError(
+      return createError(
           ErrorKind.internal, 'Cannot find url for JS script: $jsScriptId');
     }
     final locationMap = await _locations.locationForJs(url, jsLine, jsColumn);
     if (locationMap == null) {
-      return _createError(
+      return createError(
           ErrorKind.internal,
           'Cannot find Dart location for JS location: '
           'url: $url, '
@@ -187,13 +193,13 @@
     final dartSourcePath = dartLocation.uri.serverPath;
     final libraryUri = await _modules.libraryForSource(dartSourcePath);
     if (libraryUri == null) {
-      return _createError(
+      return createError(
           ErrorKind.internal, 'no libraryUri for $dartSourcePath');
     }
 
     final module = await _modules.moduleForLibrary(libraryUri.toString());
     if (module == null) {
-      return _createError(
+      return createError(
           ErrorKind.internal, 'no module for $libraryUri ($dartSourcePath)');
     }
 
@@ -228,7 +234,7 @@
     var result = await _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode);
     result = await _formatEvaluationError(result);
 
-    _logger.finest('Evaluated "$expression" to "$result"');
+    _logger.finest('Evaluated "$expression" to "${result.json}"');
     return result;
   }
 
@@ -248,10 +254,10 @@
     }
     if (error.contains('InternalError: ')) {
       error = error.replaceAll('InternalError: ', '');
-      return _createError(ErrorKind.internal, error);
+      return createError(ErrorKind.internal, error);
     }
     error = error.replaceAll(_syntheticNameFilterRegex, '');
-    return _createError(ErrorKind.compilation, error);
+    return createError(ErrorKind.compilation, error);
   }
 
   Future<RemoteObject> _formatEvaluationError(RemoteObject result) async {
@@ -259,10 +265,10 @@
       var error = '${result.value}';
       if (error.startsWith('ReferenceError: ')) {
         error = error.replaceFirst('ReferenceError: ', '');
-        return _createError(ErrorKind.reference, error);
+        return createError(ErrorKind.reference, error);
       } else if (error.startsWith('TypeError: ')) {
         error = error.replaceFirst('TypeError: ', '');
-        return _createError(ErrorKind.type, error);
+        return createError(ErrorKind.type, error);
       } else if (error.startsWith('NetworkError: ')) {
         var modulePath = _loadModuleErrorRegex.firstMatch(error)?.group(1);
         final module = modulePath != null
@@ -273,7 +279,7 @@
         error = 'Module is not loaded : $module (path: $modulePath). '
             'Accessing libraries that have not yet been used in the '
             'application is not supported during expression evaluation.';
-        return _createError(ErrorKind.loadModule, error);
+        return createError(ErrorKind.loadModule, error);
       }
     }
     return result;
diff --git a/dwds/lib/src/utilities/sdk_configuration.dart b/dwds/lib/src/utilities/sdk_configuration.dart
index 0439e01..4e20b73 100644
--- a/dwds/lib/src/utilities/sdk_configuration.dart
+++ b/dwds/lib/src/utilities/sdk_configuration.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:io';
 
 import 'package:file/file.dart';
@@ -24,11 +25,128 @@
 /// SDK configuration provider interface.
 ///
 /// Supports lazily populated configurations by allowing to create
-/// configuration asyncronously.
+/// configuration asynchronously.
 abstract class SdkConfigurationProvider {
+  const SdkConfigurationProvider();
+
   Future<SdkConfiguration> get configuration;
 }
 
+/// Sdk layout.
+///
+/// Contains definition of the default SDK layout.
+/// We keep all the path constants in one place for ease of update.
+class SdkLayout {
+  static final sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
+  static final defaultSdkLayout = createDefault(sdkDir);
+
+  static SdkLayout createDefault(String sdkDirectory) {
+    final sdkJsWeakFileName = 'dart_sdk.js';
+    final sdkJsMapWeakFileName = 'dart_sdk.js.map';
+    final sdkJsSoundFileName = 'dart_sdk_sound.js';
+    final sdkJsMapSoundFileName = 'dart_sdk_sound.js.map';
+    final sdkSummarySoundFileName = 'ddc_outline.dill';
+    final sdkSummaryWeakFileName = 'ddc_outline_unsound.dill';
+    final sdkFullDillSoundFileName = 'ddc_platform.dill';
+    final sdkFullDillWeakFileName = 'ddc_platform_unsound.dill';
+
+    final sdkSummaryDirectory = p.join(sdkDirectory, 'lib', '_internal');
+    final sdkJsDirectory =
+        p.join(sdkDirectory, 'lib', 'dev_compiler', 'kernel', 'amd');
+
+    final soundSummaryPath =
+        p.join(sdkSummaryDirectory, sdkSummarySoundFileName);
+    final soundFullDillPath =
+        p.join(sdkSummaryDirectory, sdkFullDillSoundFileName);
+    final soundJsPath = p.join(sdkJsDirectory, sdkJsSoundFileName);
+    final soundJsMapPath = p.join(sdkJsDirectory, sdkJsMapSoundFileName);
+
+    final weakSummaryPath = p.join(sdkSummaryDirectory, sdkSummaryWeakFileName);
+    final weakFullDillPath =
+        p.join(sdkSummaryDirectory, sdkFullDillWeakFileName);
+    final weakJsPath = p.join(sdkJsDirectory, sdkJsWeakFileName);
+    final weakJsMapPath = p.join(sdkJsDirectory, sdkJsMapWeakFileName);
+
+    final librariesPath = p.join(sdkDirectory, 'lib', 'libraries.json');
+    final dartdevcSnapshotPath =
+        p.join(sdkDirectory, 'bin', 'snapshots', 'dartdevc.dart.snapshot');
+    final kernelWorkerSnapshotPath =
+        p.join(sdkDirectory, 'bin', 'snapshots', 'kernel_worker.dart.snapshot');
+
+    return SdkLayout(
+      sdkJsWeakFileName: sdkJsWeakFileName,
+      sdkJsMapWeakFileName: sdkJsMapWeakFileName,
+      sdkJsSoundFileName: sdkJsSoundFileName,
+      sdkJsMapSoundFileName: sdkJsMapSoundFileName,
+      sdkSummarySoundFileName: sdkSummarySoundFileName,
+      sdkSummaryWeakFileName: sdkSummaryWeakFileName,
+      sdkFullDillSoundFileName: sdkFullDillSoundFileName,
+      sdkFullDillWeakFileName: sdkFullDillWeakFileName,
+      sdkDirectory: sdkDirectory,
+      soundSummaryPath: soundSummaryPath,
+      soundFullDillPath: soundFullDillPath,
+      soundJsPath: soundJsPath,
+      soundJsMapPath: soundJsMapPath,
+      weakSummaryPath: weakSummaryPath,
+      weakFullDillPath: weakFullDillPath,
+      weakJsPath: weakJsPath,
+      weakJsMapPath: weakJsMapPath,
+      librariesPath: librariesPath,
+      dartdevcSnapshotPath: dartdevcSnapshotPath,
+      kernelWorkerSnapshotPath: kernelWorkerSnapshotPath,
+    );
+  }
+
+  final String sdkJsWeakFileName;
+  final String sdkJsMapWeakFileName;
+  final String sdkJsSoundFileName;
+  final String sdkJsMapSoundFileName;
+  final String sdkSummarySoundFileName;
+  final String sdkSummaryWeakFileName;
+  final String sdkFullDillSoundFileName;
+  final String sdkFullDillWeakFileName;
+
+  final String sdkDirectory;
+
+  final String soundSummaryPath;
+  final String soundFullDillPath;
+  final String soundJsPath;
+  final String soundJsMapPath;
+
+  final String weakSummaryPath;
+  final String weakFullDillPath;
+  final String weakJsPath;
+  final String weakJsMapPath;
+
+  final String librariesPath;
+
+  final String dartdevcSnapshotPath;
+  final String kernelWorkerSnapshotPath;
+
+  SdkLayout({
+    required this.sdkJsWeakFileName,
+    required this.sdkJsMapWeakFileName,
+    required this.sdkJsSoundFileName,
+    required this.sdkJsMapSoundFileName,
+    required this.sdkSummarySoundFileName,
+    required this.sdkSummaryWeakFileName,
+    required this.sdkFullDillSoundFileName,
+    required this.sdkFullDillWeakFileName,
+    required this.sdkDirectory,
+    required this.soundSummaryPath,
+    required this.soundFullDillPath,
+    required this.soundJsPath,
+    required this.soundJsMapPath,
+    required this.weakSummaryPath,
+    required this.weakFullDillPath,
+    required this.weakJsPath,
+    required this.weakJsMapPath,
+    required this.librariesPath,
+    required this.dartdevcSnapshotPath,
+    required this.kernelWorkerSnapshotPath,
+  });
+}
+
 /// Data class describing the SDK layout.
 ///
 /// Provides helpers to convert paths to uris that work on all platforms.
@@ -36,30 +154,42 @@
 /// Call [validate] method to make sure the files in the configuration
 /// layout exist before reading the files.
 class SdkConfiguration {
-  // TODO(annagrin): update the tests to take those parameters
-  // and make all of the paths required (except for the compilerWorkerPath
-  // that is not used in Flutter).
+  static final defaultSdkLayout = SdkLayout.defaultSdkLayout;
+  static final defaultConfiguration =
+      SdkConfiguration.fromSdkLayout(defaultSdkLayout);
+
   String? sdkDirectory;
-  String? unsoundSdkSummaryPath;
+  String? weakSdkSummaryPath;
   String? soundSdkSummaryPath;
   String? librariesPath;
   String? compilerWorkerPath;
 
   SdkConfiguration({
     this.sdkDirectory,
-    this.unsoundSdkSummaryPath,
+    this.weakSdkSummaryPath,
     this.soundSdkSummaryPath,
     this.librariesPath,
     this.compilerWorkerPath,
   });
 
+  SdkConfiguration.empty() : this();
+
+  SdkConfiguration.fromSdkLayout(SdkLayout sdkLayout)
+      : this(
+          sdkDirectory: sdkLayout.sdkDirectory,
+          weakSdkSummaryPath: sdkLayout.weakSummaryPath,
+          soundSdkSummaryPath: sdkLayout.soundSummaryPath,
+          librariesPath: sdkLayout.librariesPath,
+          compilerWorkerPath: sdkLayout.dartdevcSnapshotPath,
+        );
+
   static Uri? _toUri(String? path) => path == null ? null : p.toUri(path);
   static Uri? _toAbsoluteUri(String? path) =>
       path == null ? null : p.toUri(p.absolute(path));
 
   Uri? get sdkDirectoryUri => _toUri(sdkDirectory);
   Uri? get soundSdkSummaryUri => _toUri(soundSdkSummaryPath);
-  Uri? get unsoundSdkSummaryUri => _toUri(unsoundSdkSummaryPath);
+  Uri? get weakSdkSummaryUri => _toUri(weakSdkSummaryPath);
   Uri? get librariesUri => _toUri(librariesPath);
 
   /// Note: has to be ///file: Uri to run in an isolate.
@@ -85,14 +215,23 @@
   }
 
   void validateSummaries({FileSystem fileSystem = const LocalFileSystem()}) {
-    if (unsoundSdkSummaryPath == null ||
-        !fileSystem.file(unsoundSdkSummaryPath).existsSync()) {
-      throw InvalidSdkConfigurationException(
-          'Sdk summary $unsoundSdkSummaryPath does not exist');
-    }
+    validateSoundSummaries(fileSystem: fileSystem);
+    validateWeakSummaries(fileSystem: fileSystem);
+  }
 
-    if (soundSdkSummaryPath == null ||
-        !fileSystem.file(soundSdkSummaryPath).existsSync()) {
+  void validateWeakSummaries(
+      {FileSystem fileSystem = const LocalFileSystem()}) {
+    if (weakSdkSummaryPath == null ||
+        !fileSystem.file(weakSdkSummaryPath).existsSync()) {
+      throw InvalidSdkConfigurationException(
+          'Sdk summary $weakSdkSummaryPath does not exist');
+    }
+  }
+
+  void validateSoundSummaries(
+      {FileSystem fileSystem = const LocalFileSystem()}) {
+    if ((soundSdkSummaryPath == null ||
+        !fileSystem.file(soundSdkSummaryPath).existsSync())) {
       throw InvalidSdkConfigurationException(
           'Sdk summary $soundSdkSummaryPath does not exist');
     }
@@ -116,27 +255,10 @@
   }
 }
 
-/// Implementation for the default SDK configuration layout.
 class DefaultSdkConfigurationProvider extends SdkConfigurationProvider {
-  DefaultSdkConfigurationProvider();
+  const DefaultSdkConfigurationProvider();
 
-  late final SdkConfiguration _configuration = _create();
-
-  /// Create and validate configuration matching the default SDK layout.
   @override
-  Future<SdkConfiguration> get configuration async => _configuration;
-
-  SdkConfiguration _create() {
-    final binDir = p.dirname(Platform.resolvedExecutable);
-    final sdkDir = p.dirname(binDir);
-
-    return SdkConfiguration(
-      sdkDirectory: sdkDir,
-      unsoundSdkSummaryPath: p.join(sdkDir, 'lib', '_internal', 'ddc_sdk.dill'),
-      soundSdkSummaryPath:
-          p.join(sdkDir, 'lib', '_internal', 'ddc_outline_sound.dill'),
-      librariesPath: p.join(sdkDir, 'lib', 'libraries.json'),
-      compilerWorkerPath: p.join(binDir, 'snapshots', 'dartdevc.dart.snapshot'),
-    );
-  }
+  Future<SdkConfiguration> get configuration async =>
+      SdkConfiguration.defaultConfiguration;
 }
diff --git a/dwds/lib/src/utilities/synchronized.dart b/dwds/lib/src/utilities/synchronized.dart
new file mode 100644
index 0000000..e318817
--- /dev/null
+++ b/dwds/lib/src/utilities/synchronized.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:pool/pool.dart';
+
+class AtomicQueue {
+  final _pool = Pool(1);
+
+  AtomicQueue();
+
+  // Executes tasks sequentially.
+  Future<T> run<T>(FutureOr<T> Function() task) => _pool.withResource(task);
+}
diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart
index 4b410c7..7b88465 100644
--- a/dwds/lib/src/version.dart
+++ b/dwds/lib/src/version.dart
@@ -1,2 +1,2 @@
 // Generated code. Do not modify.
-const packageVersion = '16.0.3';
+const packageVersion = '17.0.0';
diff --git a/dwds/mono_pkg.yaml b/dwds/mono_pkg.yaml
index d5dc531..15b5504 100644
--- a/dwds/mono_pkg.yaml
+++ b/dwds/mono_pkg.yaml
@@ -6,20 +6,53 @@
       - analyze: --fatal-infos .
       - test: test/build/ensure_version_test.dart
       sdk: dev
-    - group:
-      - analyze: .
-      - test: test/build/min_sdk_test.dart --run-skipped
-      sdk: stable
   - unit_test:
+    # Linux extension tests:
+    # Note: `Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &` must be
+    # run first for Linux.
     - group:
       - command: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
-      - test:
+      - test: --tags=extension
       sdk:
-        - stable
-    - test:
-      os: windows
+        - dev
+        - main
+      os:
+        - linux
+     # Windows extension tests:
+    - group:
+      - test: --tags=extension
       sdk:
-        - stable
+        - dev
+        - main
+      os:
+        - windows
+    # First test shard:
+    - group:
+      - test: --total-shards 3 --shard-index 0 --exclude-tags=extension
+      sdk:
+        - dev
+        - main
+      os: 
+        - linux
+        - windows
+    # Second test shard:
+    - group:
+      - test: --total-shards 3 --shard-index 1 --exclude-tags=extension
+      sdk:
+        - dev
+        - main
+      os: 
+        - linux
+        - windows
+    # Third test shard:
+    - group:
+      - test: --total-shards 3 --shard-index 2 --exclude-tags=extension
+      sdk:
+        - dev
+        - main
+      os: 
+        - linux
+        - windows
   - beta_cron:
     - analyze: .
       sdk: beta
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 2e2d46e..3a8d084 100644
--- a/dwds/pubspec.yaml
+++ b/dwds/pubspec.yaml
@@ -1,13 +1,13 @@
 name: dwds
 # Every time this changes you need to run `dart run build_runner build`.
-version: 16.0.3
+version: 17.0.0
 description: >-
   A service that proxies between the Chrome debug protocol and the Dart VM
   service protocol.
 repository: https://github.com/dart-lang/webdev/tree/master/dwds
 
 environment:
-  sdk: ">=2.19.0 <3.0.0"
+  sdk: ">=3.0.0-134.0.dev <4.0.0"
 
 dependencies:
   async: ^2.9.0
@@ -32,7 +32,7 @@
   shelf_web_socket: ^1.0.1
   source_maps: ^0.10.10
   stack_trace: ^1.10.0
-  sse: ^4.1.0
+  sse: ^4.1.2
   uuid: ^3.0.6
   vm_service: ">=10.1.0 <12.0.0"
   web_socket_channel: ^2.2.0
@@ -42,9 +42,9 @@
   args: ^2.3.1
   build: ^2.3.0
   build_daemon: ^3.1.0
-  build_runner: ^2.1.10
+  build_runner: ^2.4.0
   build_version: ^2.1.1
-  build_web_compilers: ^3.2.3
+  build_web_compilers: ^4.0.0
   built_value_generator: ^8.3.0
   graphs: ^2.1.0
   frontend_server_common:
@@ -52,6 +52,7 @@
   js: ^0.6.4
   lints: ^2.0.0
   pubspec_parse: ^1.2.0
+  puppeteer: ^2.19.0
   stream_channel: ^2.1.0
   test: ^1.21.1
   webdriver: ^3.0.0
diff --git a/dwds/web/client.dart b/dwds/web/client.dart
index c89091f..61e1cdd 100644
--- a/dwds/web/client.dart
+++ b/dwds/web/client.dart
@@ -8,11 +8,13 @@
 import 'dart:async';
 import 'dart:convert';
 import 'dart:html';
+import 'dart:js';
 
 import 'package:built_collection/built_collection.dart';
 import 'package:dwds/data/build_result.dart';
 import 'package:dwds/data/connect_request.dart';
 import 'package:dwds/data/debug_event.dart';
+import 'package:dwds/data/debug_info.dart';
 import 'package:dwds/data/devtools_request.dart';
 import 'package:dwds/data/error_response.dart';
 import 'package:dwds/data/register_event.dart';
@@ -22,6 +24,7 @@
 // NOTE(annagrin): using 'package:dwds/src/utilities/batched_stream.dart'
 // makes dart2js skip creating background.js, so we use a copy instead.
 // import 'package:dwds/src/utilities/batched_stream.dart';
+// Issue: https://github.com/dart-lang/sdk/issues/49973
 import 'package:dwds/src/web_utilities/batched_stream.dart';
 import 'package:js/js.dart';
 import 'package:sse/client/sse_client.dart';
@@ -49,7 +52,7 @@
     final fixedUri = Uri.parse(fixedPath);
     final client = fixedUri.isScheme('ws') || fixedUri.isScheme('wss')
         ? WebSocketClient(WebSocketChannel.connect(fixedUri))
-        : SseSocketClient(SseClient(fixedPath));
+        : SseSocketClient(SseClient(fixedPath, debugKey: 'InjectedClient'));
 
     Restarter restarter;
     if (dartModuleStrategy == 'require-js') {
@@ -170,7 +173,18 @@
       // If not Chromium we just invoke main, devtools aren't supported.
       runMain();
     }
-    dispatchEvent(CustomEvent('dart-app-ready'));
+    final windowContext = JsObject.fromBrowserObject(window);
+    final debugInfoJson = jsonEncode(serializers.serialize(DebugInfo((b) => b
+      ..appEntrypointPath = dartEntrypointPath
+      ..appId = windowContext['\$dartAppId']
+      ..appInstanceId = dartAppInstanceId
+      ..appOrigin = window.location.origin
+      ..appUrl = window.location.href
+      ..extensionUrl = windowContext['\$dartExtensionUri']
+      ..isInternalBuild = windowContext['\$isInternalBuild']
+      ..isFlutterApp = windowContext['\$isFlutterApp'])));
+
+    dispatchEvent(CustomEvent('dart-app-ready', detail: debugInfoJson));
   }, (error, stackTrace) {
     print('''
 Unhandled error detected in the injected client.js script.
@@ -262,4 +276,10 @@
 @JS(r'$emitRegisterEvent')
 external set emitRegisterEvent(void Function(String) func);
 
+@JS(r'$isInternalBuild')
+external bool get isInternalBuild;
+
+@JS(r'$isFlutterApp')
+external bool get isFlutterApp;
+
 bool get _isChromium => window.navigator.vendor.contains('Google');
diff --git a/fixnum/BUILD.gn b/fixnum/BUILD.gn
index f573508..0f5414b 100644
--- a/fixnum/BUILD.gn
+++ b/fixnum/BUILD.gn
@@ -1,11 +1,11 @@
-# This file is generated by package_importer.py for fixnum-1.0.1
+# This file is generated by package_importer.py for fixnum-1.1.0
 
 import("//build/dart/dart_library.gni")
 
 dart_library("fixnum") {
   package_name = "fixnum"
 
-  language_version = "2.12"
+  language_version = "2.19"
 
   disable_analysis = true
 
@@ -17,5 +17,6 @@
     "src/int32.dart",
     "src/int64.dart",
     "src/intx.dart",
+    "src/utilities.dart",
   ]
 }
diff --git a/fixnum/CHANGELOG.md b/fixnum/CHANGELOG.md
index 441c466..f731296 100644
--- a/fixnum/CHANGELOG.md
+++ b/fixnum/CHANGELOG.md
@@ -1,3 +1,14 @@
+## 1.1.0
+
+* Add `tryParseRadix`, `tryParseInt` and `tryParseHex` static methods
+  to both `Int32` and `Int64`.
+* Document exception and overflow behavior of parse functions,
+  and of `toHexString`.
+* Make `Int32` parse functions consistent with documentation (accept
+  leading minus sign, do not accept empty inputs).
+* Update the minimum SDK constraint to 2.19.
+* Update to package:lints 2.0.0.
+
 ## 1.0.1
 
 * Switch to using `package:lints`.
diff --git a/fixnum/README.md b/fixnum/README.md
index 33bdcc0..332d900 100644
--- a/fixnum/README.md
+++ b/fixnum/README.md
@@ -7,3 +7,8 @@
 Provides data types for signed 32- and 64-bit integers.
 The integer implementations in this library are designed to work identically
 whether executed on the Dart VM or compiled to JavaScript.
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/fixnum/analysis_options.yaml b/fixnum/analysis_options.yaml
index 7b7d7ce..2e3ed19 100644
--- a/fixnum/analysis_options.yaml
+++ b/fixnum/analysis_options.yaml
@@ -1,8 +1,8 @@
 include: package:lints/recommended.yaml
 
 analyzer:
-  strong-mode:
-    implicit-casts: false
+  language:
+    strict-casts: true
 
 linter:
   rules:
@@ -15,7 +15,6 @@
     - cascade_invocations
     - comment_references
     - directives_ordering
-    - invariant_booleans
     - join_return_with_assignment
     - lines_longer_than_80_chars
     - missing_whitespace_between_adjacent_strings
diff --git a/fixnum/lib/fixnum.dart b/fixnum/lib/fixnum.dart
index 72f9742..eeb6def 100644
--- a/fixnum/lib/fixnum.dart
+++ b/fixnum/lib/fixnum.dart
@@ -8,6 +8,6 @@
 /// identically whether executed on the Dart VM or compiled to JavaScript.
 library fixnum;
 
-part 'src/intx.dart';
-part 'src/int32.dart';
-part 'src/int64.dart';
+export 'src/int32.dart';
+export 'src/int64.dart';
+export 'src/intx.dart';
diff --git a/fixnum/lib/src/int32.dart b/fixnum/lib/src/int32.dart
index 760bc22..8045bc1 100644
--- a/fixnum/lib/src/int32.dart
+++ b/fixnum/lib/src/int32.dart
@@ -4,7 +4,9 @@
 
 // ignore_for_file: constant_identifier_names
 
-part of fixnum;
+import 'int64.dart';
+import 'intx.dart';
+import 'utilities.dart' as u;
 
 /// An immutable 32-bit signed integer, in the range [-2^31, 2^31 - 1].
 /// Arithmetic operations may overflow in order to maintain this range.
@@ -26,92 +28,138 @@
   /// An [Int32] constant equal to 2.
   static const Int32 TWO = Int32._internal(2);
 
-  // Hex digit char codes
-  static const int _CC_0 = 48; // '0'.codeUnitAt(0)
-  static const int _CC_9 = 57; // '9'.codeUnitAt(0)
-  static const int _CC_a = 97; // 'a'.codeUnitAt(0)
-  static const int _CC_z = 122; // 'z'.codeUnitAt(0)
-  static const int _CC_A = 65; // 'A'.codeUnitAt(0)
-  static const int _CC_Z = 90; // 'Z'.codeUnitAt(0)
+  // Mask to 32-bits.
+  static const int _MASK_U32 = 0xFFFFFFFF;
 
-  static int _decodeDigit(int c) {
-    if (c >= _CC_0 && c <= _CC_9) {
-      return c - _CC_0;
-    } else if (c >= _CC_a && c <= _CC_z) {
-      return c - _CC_a + 10;
-    } else if (c >= _CC_A && c <= _CC_Z) {
-      return c - _CC_A + 10;
-    } else {
-      return -1; // bad char code
-    }
-  }
+  /// Parses [source] in a given [radix] between 2 and 36.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of base-[radix]
+  /// digits (using letters from `a` to `z` as digits with values 10 through
+  /// 25 for radixes above 10), possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// integer numeral in base [radix].
+  static Int32 parseRadix(String source, int radix) =>
+      _parseRadix(source, u.validateRadix(radix), true)!;
 
-  static int _validateRadix(int radix) {
-    if (2 <= radix && radix <= 36) return radix;
-    throw RangeError.range(radix, 2, 36, 'radix');
-  }
+  /// Parses [source] in a given [radix] between 2 and 36.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of base-[radix]
+  /// digits (using letters from `a` to `z` as digits with values 10 through
+  /// 25 for radixes above 10), possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// integer numeral in base [radix].
+  static Int32? tryParseRadix(String source, int radix) =>
+      _parseRadix(source, u.validateRadix(radix), false);
 
-  /// Parses a [String] in a given [radix] between 2 and 16 and returns an
-  /// [Int32].
   // TODO(rice) - Make this faster by converting several digits at once.
-  static Int32 parseRadix(String s, int radix) {
-    _validateRadix(radix);
-    var x = ZERO;
-    for (var i = 0; i < s.length; i++) {
-      var c = s.codeUnitAt(i);
-      var digit = _decodeDigit(c);
-      if (digit < 0 || digit >= radix) {
-        throw FormatException('Non-radix code unit: $c');
-      }
-      x = (x * radix) + digit as Int32;
+  static Int32? _parseRadix(String s, int radix, bool throwOnError) {
+    var index = 0;
+    var negative = false;
+    if (s.startsWith('-')) {
+      negative = true;
+      index = 1;
     }
-    return x;
+    if (index == s.length) {
+      if (!throwOnError) return null;
+      throw FormatException('No digits', s, index);
+    }
+    var result = 0;
+    for (; index < s.length; index++) {
+      var c = s.codeUnitAt(index);
+      var digit = u.decodeDigit(c);
+      if (digit < radix) {
+        /// Doesn't matter whether the result is unsigned
+        /// or signed (as on the web), only the bits matter
+        /// to the [Int32.new] constructor.
+        result = (result * radix + digit) & _MASK_U32;
+      } else {
+        if (!throwOnError) return null;
+        throw FormatException('Non radix code unit', s, index);
+      }
+    }
+    if (negative) result = -result;
+    return Int32(result);
   }
 
-  /// Parses a decimal [String] and returns an [Int32].
-  static Int32 parseInt(String s) => Int32(int.parse(s));
+  /// Parses [source] as a decimal numeral.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of digits (`0`-`9`),
+  /// possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// decimal integer numeral.
+  static Int32 parseInt(String source) => _parseRadix(source, 10, true)!;
 
-  /// Parses a hexadecimal [String] and returns an [Int32].
-  static Int32 parseHex(String s) => parseRadix(s, 16);
+  /// Parses [source] as a decimal numeral.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of digits (`0`-`9`),
+  /// possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// decimal integer numeral.
+  static Int32? tryParseInt(String source) => _parseRadix(source, 10, false);
 
-  // Assumes i is <= 32-bit.
-  static int _bitCount(int i) {
-    // See "Hacker's Delight", section 5-1, "Counting 1-Bits".
+  /// Parses [source] as a hexadecimal numeral.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of hexadecimal
+  /// digits (`0`-`9`, `a`-`f` or `A`-`F`), possibly prefixed by a `-` sign.
+  ///
+  /// Returns `null` if the input is not a valid
+  /// hexadecimal integer numeral.
+  static Int32 parseHex(String source) => _parseRadix(source, 16, true)!;
 
-    // The basic strategy is to use "divide and conquer" to
-    // add pairs (then quads, etc.) of bits together to obtain
-    // sub-counts.
-    //
-    // A straightforward approach would look like:
-    //
-    // i = (i & 0x55555555) + ((i >>  1) & 0x55555555);
-    // i = (i & 0x33333333) + ((i >>  2) & 0x33333333);
-    // i = (i & 0x0F0F0F0F) + ((i >>  4) & 0x0F0F0F0F);
-    // i = (i & 0x00FF00FF) + ((i >>  8) & 0x00FF00FF);
-    // i = (i & 0x0000FFFF) + ((i >> 16) & 0x0000FFFF);
-    //
-    // The code below removes unnecessary &'s and uses a
-    // trick to remove one instruction in the first line.
-
-    i -= (i >> 1) & 0x55555555;
-    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
-    i = (i + (i >> 4)) & 0x0F0F0F0F;
-    i += i >> 8;
-    i += i >> 16;
-    return i & 0x0000003F;
-  }
-
-  // Assumes i is <= 32-bit
-  static int _numberOfLeadingZeros(int i) {
-    i |= i >> 1;
-    i |= i >> 2;
-    i |= i >> 4;
-    i |= i >> 8;
-    i |= i >> 16;
-    return _bitCount(~i);
-  }
-
-  static int _numberOfTrailingZeros(int i) => _bitCount((i & -i) - 1);
+  /// Parses [source] as a hexadecimal numeral.
+  ///
+  /// Returns an [Int32] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 32 bit integer,
+  /// the numerical value is truncated to the lowest 32 bits
+  /// of the value's binary representation,
+  /// interpreted as a 32-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of hexadecimal
+  /// digits (`0`-`9`, `a`-`f` or `A`-`F`), possibly prefixed by a `-` sign.
+  ///
+  /// Returns `null` if the input is not a valid
+  /// hexadecimal integer numeral.
+  static Int32? tryParseHex(String source) => _parseRadix(source, 16, false);
 
   // The internal value, kept in the range [MIN_VALUE, MAX_VALUE].
   final int _i;
@@ -130,7 +178,7 @@
     } else if (val is int) {
       return val;
     }
-    throw ArgumentError(val);
+    throw ArgumentError.value(val, 'other', 'Not an int, Int32 or Int64');
   }
 
   // The +, -, * , &, |, and ^ operaters deal with types as follows:
@@ -369,10 +417,10 @@
   }
 
   @override
-  int numberOfLeadingZeros() => _numberOfLeadingZeros(_i);
+  int numberOfLeadingZeros() => u.numberOfLeadingZeros(_i);
 
   @override
-  int numberOfTrailingZeros() => _numberOfTrailingZeros(_i);
+  int numberOfTrailingZeros() => u.numberOfTrailingZeros(_i);
 
   @override
   Int32 toSigned(int width) {
diff --git a/fixnum/lib/src/int64.dart b/fixnum/lib/src/int64.dart
index 307a08f..e4e2ba4 100644
--- a/fixnum/lib/src/int64.dart
+++ b/fixnum/lib/src/int64.dart
@@ -9,7 +9,9 @@
 //
 // ignore_for_file: omit_local_variable_types
 
-part of fixnum;
+import 'int32.dart';
+import 'intx.dart';
+import 'utilities.dart' as u;
 
 /// An immutable 64-bit signed integer, in the range [-2^63, 2^63 - 1].
 /// Arithmetic operations may overflow in order to maintain this range.
@@ -57,45 +59,76 @@
   /// is performed.
   const Int64._bits(this._l, this._m, this._h);
 
-  /// Parses a [String] in a given [radix] between 2 and 36 and returns an
-  /// [Int64].
-  static Int64 parseRadix(String s, int radix) =>
-      _parseRadix(s, Int32._validateRadix(radix));
+  /// Parses [source] in a given [radix] between 2 and 36.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of base-[radix]
+  /// digits (using letters from `a` to `z` as digits with values 10 through
+  /// 25 for radixes above 10), possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not recognized as a valid
+  /// integer numeral.
+  static Int64 parseRadix(String source, int radix) =>
+      _parseRadix(source, u.validateRadix(radix), true)!;
 
-  static Int64 _parseRadix(String s, int radix) {
+  /// Parses [source] in a given [radix] between 2 and 36.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of base-[radix]
+  /// digits (using letters from `a` to `z` as digits with values 10 through
+  /// 25 for radixes above 10), possibly prefixed by a `-` sign.
+  ///
+  /// Returns `null` if the input is not recognized as a valid
+  /// integer numeral.
+  static Int64? tryParseRadix(String source, int radix) =>
+      _parseRadix(source, u.validateRadix(radix), false);
+
+  static Int64? _parseRadix(String s, int radix, bool throwOnError) {
     int i = 0;
     bool negative = false;
-    if (i < s.length && s[0] == '-') {
+    if (s.startsWith('-')) {
       negative = true;
       i++;
     }
 
-    // TODO(https://github.com/dart-lang/sdk/issues/38728). Replace with "if (i
-    // >= s.length)".
-    if (!(i < s.length)) {
-      throw FormatException("No digits in '$s'");
+    if (i >= s.length) {
+      if (!throwOnError) return null;
+      throw FormatException('No digits', s, i);
     }
 
     int d0 = 0, d1 = 0, d2 = 0; //  low, middle, high components.
     for (; i < s.length; i++) {
       int c = s.codeUnitAt(i);
-      int digit = Int32._decodeDigit(c);
-      if (digit < 0 || digit >= radix) {
-        throw FormatException('Non-radix char code: $c');
+      int digit = u.decodeDigit(c);
+      if (digit < radix) {
+        // [radix] and [digit] are at most 6 bits, component is 22, so we can
+        // multiply and add within 30 bit temporary values.
+        d0 = d0 * radix + digit;
+        int carry = d0 >> _BITS;
+        d0 = _MASK & d0;
+
+        d1 = d1 * radix + carry;
+        carry = d1 >> _BITS;
+        d1 = _MASK & d1;
+
+        d2 = d2 * radix + carry;
+        d2 = _MASK2 & d2;
+      } else {
+        if (!throwOnError) return null;
+        throw FormatException('Not radix digit', s, i);
       }
-
-      // [radix] and [digit] are at most 6 bits, component is 22, so we can
-      // multiply and add within 30 bit temporary values.
-      d0 = d0 * radix + digit;
-      int carry = d0 >> _BITS;
-      d0 = _MASK & d0;
-
-      d1 = d1 * radix + carry;
-      carry = d1 >> _BITS;
-      d1 = _MASK & d1;
-
-      d2 = d2 * radix + carry;
-      d2 = _MASK2 & d2;
     }
 
     if (negative) return _negate(d0, d1, d2);
@@ -103,11 +136,69 @@
     return Int64._masked(d0, d1, d2);
   }
 
-  /// Parses a decimal [String] and returns an [Int64].
-  static Int64 parseInt(String s) => _parseRadix(s, 10);
+  /// Parses [source] as a decimal numeral.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of digits (`0`-`9`),
+  /// possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// decimal integer numeral.
+  static Int64 parseInt(String source) => _parseRadix(source, 10, true)!;
 
-  /// Parses a hexadecimal [String] and returns an [Int64].
-  static Int64 parseHex(String s) => _parseRadix(s, 16);
+  /// Parses [source] as a decimal numeral.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of digits (`0`-`9`),
+  /// possibly prefixed by a `-` sign.
+  ///
+  /// Returns `null` if the input is not a valid
+  /// decimal integer numeral.
+  static Int64? tryParseInt(String source) => _parseRadix(source, 10, false);
+
+  /// Parses [source] as a hexadecimal numeral.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of hexadecimal
+  /// digits (`0`-`9`, `a`-`f` or `A`-`F`), possibly prefixed by a `-` sign.
+  ///
+  /// Throws a [FormatException] if the input is not a valid
+  /// hexadecimal integer numeral.
+  static Int64 parseHex(String source) => _parseRadix(source, 16, true)!;
+
+  /// Parses [source] as a hexadecimal numeral.
+  ///
+  /// Returns an [Int64] with the numerical value of [source].
+  /// If the numerical value of [source] does not fit
+  /// in a signed 64 bit integer,
+  /// the numerical value is truncated to the lowest 64 bits
+  /// of the value's binary representation,
+  /// interpreted as a 64-bit two's complement integer.
+  ///
+  /// The [source] string must contain a sequence of hexadecimal
+  /// digits (`0`-`9`, `a`-`f` or `A`-`F`), possibly prefixed by a `-` sign.
+  ///
+  /// Returns `null` if the input is not a valid
+  /// hexadecimal integer numeral.
+  static Int64? tryParseHex(String source) => _parseRadix(source, 16, false);
 
   //
   // Public constructors
@@ -135,43 +226,32 @@
   }
 
   factory Int64.fromBytes(List<int> bytes) {
-    int top = bytes[7] & 0xff;
-    top <<= 8;
-    top |= bytes[6] & 0xff;
-    top <<= 8;
-    top |= bytes[5] & 0xff;
-    top <<= 8;
-    top |= bytes[4] & 0xff;
-
-    int bottom = bytes[3] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[2] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[1] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[0] & 0xff;
-
-    return Int64.fromInts(top, bottom);
+    // 20 bits into top, 22 into middle and bottom.
+    var split1 = bytes[5] & 0xFF;
+    var high =
+        ((bytes[7] & 0xFF) << 12) | ((bytes[6] & 0xFF) << 4) | (split1 >> 4);
+    var split2 = bytes[2] & 0xFF;
+    var middle = (split1 << 18) |
+        ((bytes[4] & 0xFF) << 10) |
+        ((bytes[3] & 0xFF) << 2) |
+        (split2 >> 6);
+    var low = (split2 << 16) | ((bytes[1] & 0xFF) << 8) | (bytes[0] & 0xFF);
+    // Top bits from above will be masked off here.
+    return Int64._masked(low, middle, high);
   }
 
   factory Int64.fromBytesBigEndian(List<int> bytes) {
-    int top = bytes[0] & 0xff;
-    top <<= 8;
-    top |= bytes[1] & 0xff;
-    top <<= 8;
-    top |= bytes[2] & 0xff;
-    top <<= 8;
-    top |= bytes[3] & 0xff;
-
-    int bottom = bytes[4] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[5] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[6] & 0xff;
-    bottom <<= 8;
-    bottom |= bytes[7] & 0xff;
-
-    return Int64.fromInts(top, bottom);
+    var split1 = bytes[2] & 0xFF;
+    var high =
+        ((bytes[0] & 0xFF) << 12) | ((bytes[1] & 0xFF) << 4) | (split1 >> 4);
+    var split2 = bytes[5] & 0xFF;
+    var middle = (split1 << 18) |
+        ((bytes[3] & 0xFF) << 10) |
+        ((bytes[4] & 0xFF) << 2) |
+        (split2 >> 6);
+    var low = (split2 << 16) | ((bytes[6] & 0xFF) << 8) | (bytes[7] & 0xFF);
+    // Top bits from above will be masked off here.
+    return Int64._masked(low, middle, high);
   }
 
   /// Constructs an [Int64] from a pair of 32-bit integers having the value
@@ -195,7 +275,7 @@
     } else if (value is Int32) {
       return value.toInt64();
     }
-    throw ArgumentError.value(value);
+    throw ArgumentError.value(value, 'other', 'not an int, Int32 or Int64');
   }
 
   @override
@@ -558,11 +638,11 @@
   /// between 0 and 64.
   @override
   int numberOfLeadingZeros() {
-    int b2 = Int32._numberOfLeadingZeros(_h);
+    int b2 = u.numberOfLeadingZeros(_h);
     if (b2 == 32) {
-      int b1 = Int32._numberOfLeadingZeros(_m);
+      int b1 = u.numberOfLeadingZeros(_m);
       if (b1 == 32) {
-        return Int32._numberOfLeadingZeros(_l) + 32;
+        return u.numberOfLeadingZeros(_l) + 32;
       } else {
         return b1 + _BITS2 - (32 - _BITS);
       }
@@ -575,17 +655,17 @@
   /// between 0 and 64.
   @override
   int numberOfTrailingZeros() {
-    int zeros = Int32._numberOfTrailingZeros(_l);
+    int zeros = u.numberOfTrailingZeros(_l);
     if (zeros < 32) {
       return zeros;
     }
 
-    zeros = Int32._numberOfTrailingZeros(_m);
+    zeros = u.numberOfTrailingZeros(_m);
     if (zeros < 32) {
       return _BITS + zeros;
     }
 
-    zeros = Int32._numberOfTrailingZeros(_h);
+    zeros = u.numberOfTrailingZeros(_h);
     if (zeros < 32) {
       return _BITS01 + zeros;
     }
@@ -672,7 +752,6 @@
   @override
   String toString() => _toRadixString(10);
 
-  // TODO(rice) - Make this faster by avoiding arithmetic.
   @override
   String toHexString() {
     if (isZero) return '0';
@@ -692,11 +771,10 @@
 
   @pragma('dart2js:noInline')
   String toRadixStringUnsigned(int radix) =>
-      _toRadixStringUnsigned(Int32._validateRadix(radix), _l, _m, _h, '');
+      _toRadixStringUnsigned(u.validateRadix(radix), _l, _m, _h, '');
 
   @override
-  String toRadixString(int radix) =>
-      _toRadixString(Int32._validateRadix(radix));
+  String toRadixString(int radix) => _toRadixString(u.validateRadix(radix));
 
   String _toRadixString(int radix) {
     int d0 = _l;
@@ -861,8 +939,8 @@
 
   String toDebugString() => 'Int64[_l=$_l, _m=$_m, _h=$_h]';
 
-  static Int64 _masked(int a0, int a1, int a2) =>
-      Int64._bits(_MASK & a0, _MASK & a1, _MASK2 & a2);
+  static Int64 _masked(int low, int medium, int high) =>
+      Int64._bits(_MASK & low, _MASK & medium, _MASK2 & high);
 
   static Int64 _sub(int a0, int a1, int a2, int b0, int b1, int b2) {
     int diff0 = a0 - b0;
@@ -893,8 +971,7 @@
   static Int64 _divide(Int64 a, other, int what) {
     Int64 b = _promote(other);
     if (b.isZero) {
-      // ignore: deprecated_member_use
-      throw const IntegerDivisionByZeroException();
+      throw UnsupportedError('Division by zero');
     }
     if (a.isZero) return ZERO;
 
diff --git a/fixnum/lib/src/intx.dart b/fixnum/lib/src/intx.dart
index bedb3e1..d51a583 100644
--- a/fixnum/lib/src/intx.dart
+++ b/fixnum/lib/src/intx.dart
@@ -2,7 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-part of fixnum;
+import 'int32.dart';
+import 'int64.dart';
 
 /// A fixed-precision integer.
 abstract class IntX implements Comparable<Object> {
@@ -188,7 +189,14 @@
   String toString();
 
   /// Returns a string representing the value of this integer in hexadecimal
-  /// notation; example: `'0xd'`.
+  /// notation.
+  ///
+  /// Example: `Int64(0xf01d).toHexString()` returns `'F01D'`.
+  ///
+  /// The string may interprets the number as *unsigned*, and has no leading
+  /// minus, even if the value [isNegative].
+  ///
+  /// Example: `Int64(-1).toHexString()` returns `'FFFFFFFFFFFFFFFF'`.
   String toHexString();
 
   /// Returns a string representing the value of this integer in the given
diff --git a/fixnum/lib/src/utilities.dart b/fixnum/lib/src/utilities.dart
new file mode 100644
index 0000000..d603b57
--- /dev/null
+++ b/fixnum/lib/src/utilities.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Shared functionality used by multiple classes and their implementations.
+
+int validateRadix(int radix) =>
+    RangeError.checkValueInInterval(radix, 2, 36, 'radix');
+
+/// Converts radix digits into their numeric values.
+///
+/// Converts the characters `0`-`9` into the values 0 through 9,
+/// and the letters `a`-`z` or `A`-`Z` into values 10 through 35,
+/// and return that value.
+/// Any other character returns a value above 35, which means it's
+/// not a valid digit in any radix in the range 2 through 36.
+int decodeDigit(int c) {
+  // Hex digit char codes
+  const int c0 = 48; // '0'.codeUnitAt(0)
+  const int ca = 97; // 'a'.codeUnitAt(0)
+
+  int digit = c ^ c0;
+  if (digit < 10) return digit;
+  int letter = (c | 0x20) - ca;
+  if (letter >= 0) {
+    // Returns values above 36 for invalid digits.
+    // The value is checked against the actual radix where the return
+    // value is used, so this is safe.
+    return letter + 10;
+  } else {
+    return 255; // Never a valid radix.
+  }
+}
+
+// Assumes i is <= 32-bit
+int numberOfLeadingZeros(int i) {
+  i |= i >> 1;
+  i |= i >> 2;
+  i |= i >> 4;
+  i |= i >> 8;
+  i |= i >> 16;
+  return bitCount(~i);
+}
+
+int numberOfTrailingZeros(int i) => bitCount((i & -i) - 1);
+
+// Assumes i is <= 32-bit.
+int bitCount(int i) {
+  // See "Hacker's Delight", section 5-1, "Counting 1-Bits".
+
+  // The basic strategy is to use "divide and conquer" to
+  // add pairs (then quads, etc.) of bits together to obtain
+  // sub-counts.
+  //
+  // A straightforward approach would look like:
+  //
+  // i = (i & 0x55555555) + ((i >>  1) & 0x55555555);
+  // i = (i & 0x33333333) + ((i >>  2) & 0x33333333);
+  // i = (i & 0x0F0F0F0F) + ((i >>  4) & 0x0F0F0F0F);
+  // i = (i & 0x00FF00FF) + ((i >>  8) & 0x00FF00FF);
+  // i = (i & 0x0000FFFF) + ((i >> 16) & 0x0000FFFF);
+  //
+  // The code below removes unnecessary &'s and uses a
+  // trick to remove one instruction in the first line.
+
+  i -= (i >> 1) & 0x55555555;
+  i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
+  i = (i + (i >> 4)) & 0x0F0F0F0F;
+  i += i >> 8;
+  i += i >> 16;
+  return i & 0x0000003F;
+}
diff --git a/fixnum/pubspec.yaml b/fixnum/pubspec.yaml
index 7353636..3180c25 100644
--- a/fixnum/pubspec.yaml
+++ b/fixnum/pubspec.yaml
@@ -1,13 +1,13 @@
 name: fixnum
-version: 1.0.1
+version: 1.1.0
 description: >-
   Library for 32- and 64-bit signed fixed-width integers with consistent
   behavior between native and JS runtimes.
 repository: https://github.com/dart-lang/fixnum
 
 environment:
-  sdk: '>=2.12.0 <3.0.0'
+  sdk: '>=2.19.0 <3.0.0'
 
 dev_dependencies:
-  lints: ^1.0.0
+  lints: ^2.0.0
   test: ^1.16.0
diff --git a/meta/BUILD.gn b/meta/BUILD.gn
index 808fec1..8d41990 100644
--- a/meta/BUILD.gn
+++ b/meta/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by package_importer.py for meta-1.8.0
+# This file is generated by package_importer.py for meta-1.9.0
 
 import("//build/dart/dart_library.gni")
 
diff --git a/meta/CHANGELOG.md b/meta/CHANGELOG.md
index 1d01881..9efa6a9 100644
--- a/meta/CHANGELOG.md
+++ b/meta/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 1.9.0
+
+* Introduce `@reopen` to annotate class or mixin declarations that can safely
+  extend classes marked `base`, `final` or `interface`.
+* Introduce `@MustBeOverridden` to annotate class or mixin members which must be
+  overridden in all subclasses.
+* Deprecate `@alwaysThrows`, which can be replaced by using a return type of
+  'Never'.
+
 ## 1.8.0
 
 * Add `@UseResult.unless`.
diff --git a/meta/analysis_options.yaml b/meta/analysis_options.yaml
new file mode 100644
index 0000000..572dd23
--- /dev/null
+++ b/meta/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/meta/lib/meta.dart b/meta/lib/meta.dart
index ba14be9..93dc857 100644
--- a/meta/lib/meta.dart
+++ b/meta/lib/meta.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+// ignore_for_file: library_private_types_in_public_api
+
 /// Annotations that developers can use to express the intentions that otherwise
 /// can't be deduced by statically analyzing the source code.
 ///
@@ -49,6 +51,7 @@
 /// Tools, such as the analyzer, can also expect this contract to be enforced;
 /// that is, tools may emit warnings if a function with this annotation
 /// _doesn't_ always throw.
+@Deprecated("Use a return type of 'Never' instead")
 const _AlwaysThrows alwaysThrows = _AlwaysThrows();
 
 /// Used to annotate a parameter of an instance method that overrides another
@@ -169,19 +172,40 @@
 ///   constructor is not a compile-time constant.
 const _Literal literal = _Literal();
 
-/// Used to annotate an instance method `m`. Indicates that every invocation of
-/// a method that overrides `m` must also invoke `m`. In addition, every method
-/// that overrides `m` is implicitly annotated with this same annotation.
+/// Used to annotate an instance member `m` declared on a class or mixin `C`.
+/// Indicates that every subclass of `C`, concrete or abstract, must directly
+/// override `m`.
 ///
-/// Note that private methods with this annotation cannot be validly overridden
-/// outside of the library that defines the annotated method.
+/// This annotation places no restrictions on the overriding members. In
+/// particular, it does not require that the overriding members invoke the
+/// overridden member. The annotation [mustCallSuper] can be used to add that
+/// requirement.
 ///
 /// Tools, such as the analyzer, can provide feedback if
 ///
-/// * the annotation is associated with anything other than an instance method,
+/// * the annotation is associated with anything other than an instance member
+///   (a method, operator, field, getter, or setter) of a class or of a mixin,
 ///   or
-/// * a method that overrides a method that has this annotation can return
-///   without invoking the overridden method.
+/// * the annotation is associated with a member `m` in class or mixin `C`, and
+///   there is a class or mixin `D` which is a subclass of `C` (directly or
+///   indirectly), and `D` does not directly declare a concrete override of `m`
+///   and does not directly declare a concrete override of `noSuchMethod`.
+const _MustBeOverridden mustBeOverridden = _MustBeOverridden();
+
+/// Used to annotate an instance member (method, getter, setter, operator, or
+/// field) `m`. Indicates that every invocation of a member that overrides `m`
+/// must also invoke `m`. In addition, every method that overrides `m` is
+/// implicitly annotated with this same annotation.
+///
+/// Note that private members with this annotation cannot be validly overridden
+/// outside of the library that defines the annotated member.
+///
+/// Tools, such as the analyzer, can provide feedback if
+///
+/// * the annotation is associated with anything other than an instance member,
+///   or
+/// * a member that overrides a member that has this annotation can return
+///   without invoking the overridden member.
 const _MustCallSuper mustCallSuper = _MustCallSuper();
 
 /// Used to annotate an instance member (method, getter, setter, operator, or
@@ -244,6 +268,44 @@
 // "referenced."
 const _Protected protected = _Protected();
 
+/// Annotation for intentionally loosening restrictions on subtyping.
+///
+/// Indicates that the annotated class or mixin declaration
+/// intentionally allows subclasses to implement or extend it, even
+/// though it has a superclass which does not allow that.
+///
+/// A declaration annotated with `@reopen` will not generate warnings from the
+/// `implicit_reopen` lint. That lint will otherwise warn when a subclass *C*
+/// removes some of the restrictions that a superclass has.
+///
+/// * A class or mixin prevents inheritance if it's marked interface, or if it
+///   is marked sealed and it extends or mixes in another class which prevents
+///   inheritance.
+/// * We give a warning if a subclass extends or mixes in another class which
+///   prevents inheritance, and the subclass is marked base, or is not marked
+///   `final`, `interface` or `sealed`.
+/// * A class or mixin requires inheritance if it's marked `base`, or if it is
+///   marked `sealed` and it extends or mixes in another class or mixin which
+///   requires inheritance.
+/// * We give a warning if a subclass extends or mixes in another class which
+///   requires inheritance, and the subclass has no modifier or is marked
+///   `interface`.
+/// * A class or mixin prevents subclassing if it's marked `final`, or if it is
+///   marked `sealed` and it extends, mixes in, or implements the interface of
+///   another class or mixin which prevents subclassing.
+/// * We give a warning if a subclass or sub-mixin extends, mixes in, implements
+///   the interface of, or has as an on type a class or mixin which prevents
+///   subclassing, and the subclass or sub-mixin has no modifier or is marked
+///   `interface` or `base`.
+///
+/// In addition, tools, such as the analyzer, can provide feedback if
+///
+/// * The annotation is applied to anything other than a class or mixin.
+/// * The annotation is applied to a class or mixin which does not require it.
+///   (The intent to reopen was not satisfied.)
+@experimental // todo(pq): remove before publishing for 3.0 (https://github.com/dart-lang/sdk/issues/51059)
+const _Reopen reopen = _Reopen();
+
 /// Used to annotate a named parameter `p` in a method or function `f`.
 /// Indicates that every invocation of `f` must include an argument
 /// corresponding to `p`, despite the fact that `p` would otherwise be an
@@ -425,6 +487,22 @@
   const _Literal();
 }
 
+@Target({
+  TargetKind.field,
+  TargetKind.getter,
+  TargetKind.method,
+  TargetKind.setter,
+})
+class _MustBeOverridden {
+  const _MustBeOverridden();
+}
+
+@Target({
+  TargetKind.field,
+  TargetKind.getter,
+  TargetKind.method,
+  TargetKind.setter,
+})
 class _MustCallSuper {
   const _MustCallSuper();
 }
@@ -449,6 +527,14 @@
   const _Protected();
 }
 
+@Target({
+  TargetKind.classType,
+  TargetKind.mixinType,
+})
+class _Reopen {
+  const _Reopen();
+}
+
 class _Sealed {
   const _Sealed();
 }
diff --git a/meta/pubspec.yaml b/meta/pubspec.yaml
index 4c5deeb..8e42bc2 100644
--- a/meta/pubspec.yaml
+++ b/meta/pubspec.yaml
@@ -1,6 +1,6 @@
 name: meta
 # Note, because version `2.0.0` was mistakenly released, the next major version must be `3.x.y`.
-version: 1.8.0
+version: 1.9.0
 description: >-
  Annotations used to express developer intentions that can't otherwise be
  deduced by statically analyzing source code.
@@ -8,3 +8,10 @@
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
+
+# We use 'any' version constraints here as we get our package versions from
+# the dart-lang/sdk repo's DEPS file. Note that this is a special case; the
+# best practice for packages is to specify their compatible version ranges.
+# See also https://dart.dev/tools/pub/dependencies.
+dev_dependencies:
+  lints: any
diff --git a/multicast_dns/BUILD.gn b/multicast_dns/BUILD.gn
index 5eb612d..c0b16f9 100644
--- a/multicast_dns/BUILD.gn
+++ b/multicast_dns/BUILD.gn
@@ -1,11 +1,11 @@
-# This file is generated by package_importer.py for multicast_dns-0.3.2+2
+# This file is generated by package_importer.py for multicast_dns-0.3.2+3
 
 import("//build/dart/dart_library.gni")
 
 dart_library("multicast_dns") {
   package_name = "multicast_dns"
 
-  language_version = "2.14"
+  language_version = "2.17"
 
   disable_analysis = true
 
diff --git a/multicast_dns/CHANGELOG.md b/multicast_dns/CHANGELOG.md
index 76837a4..55ee390 100644
--- a/multicast_dns/CHANGELOG.md
+++ b/multicast_dns/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.3.2+3
+
+* Removes use of `runtimeType.toString()`.
+* Updates minimum SDK version to Flutter 3.0.
+
 ## 0.3.2+2
 
 * Fixes lints warnings.
diff --git a/multicast_dns/lib/src/resource_record.dart b/multicast_dns/lib/src/resource_record.dart
index 65320d5..d58da79 100644
--- a/multicast_dns/lib/src/resource_record.dart
+++ b/multicast_dns/lib/src/resource_record.dart
@@ -167,7 +167,7 @@
 
   @override
   String toString() =>
-      '$runtimeType{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}';
+      'ResourceRecordQuery{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}';
 }
 
 /// Base implementation of DNS resource records (RRs).
diff --git a/multicast_dns/pubspec.yaml b/multicast_dns/pubspec.yaml
index 04ed3aa..8aa9b77 100644
--- a/multicast_dns/pubspec.yaml
+++ b/multicast_dns/pubspec.yaml
@@ -2,10 +2,10 @@
 description: Dart package for performing mDNS queries (e.g. Bonjour, Avahi).
 repository: https://github.com/flutter/packages/tree/main/packages/multicast_dns
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+multicast_dns%22
-version: 0.3.2+2
+version: 0.3.2+3
 
 environment:
-  sdk: ">=2.14.0 <3.0.0"
+  sdk: ">=2.17.0 <3.0.0"
 
 dependencies:
   meta: ^1.3.0
diff --git a/package_config.json b/package_config.json
index 777e3cc..5861dbe 100644
--- a/package_config.json
+++ b/package_config.json
@@ -159,7 +159,7 @@
       "rootUri": "./devtools_shared/"
     },
     {
-      "languageVersion": "2.19",
+      "languageVersion": "3.0",
       "name": "dwds",
       "packageUri": "lib/",
       "rootUri": "./dwds/"
@@ -189,7 +189,7 @@
       "rootUri": "./file/"
     },
     {
-      "languageVersion": "2.12",
+      "languageVersion": "2.19",
       "name": "fixnum",
       "packageUri": "lib/",
       "rootUri": "./fixnum/"
@@ -369,7 +369,7 @@
       "rootUri": "./mockito/"
     },
     {
-      "languageVersion": "2.14",
+      "languageVersion": "2.17",
       "name": "multicast_dns",
       "packageUri": "lib/",
       "rootUri": "./multicast_dns/"