diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71cd11..ed6c085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,3 +61,47 @@ jobs: - name: Test Dart run: dart test + + bridge-release: + name: Bridge release package + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Set up Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Install web dependencies + working-directory: apps/web + run: npm ci --legacy-peer-deps + + - name: Build packaged Web workbench + working-directory: apps/web + run: npm run build:bridge + + - name: Install bridge dependencies + working-directory: apps/bridge + run: dart pub get + + - name: Test bridge release helpers + working-directory: apps/bridge + run: dart test test/release + + - name: Validate bridge package layout + working-directory: apps/bridge + run: dart tool/validate_release_layout.dart + + - name: Validate pub package dry run + working-directory: apps/bridge + run: dart pub publish --dry-run diff --git a/.github/workflows/publish_bridge.yml b/.github/workflows/publish_bridge.yml new file mode 100644 index 0000000..2872266 --- /dev/null +++ b/.github/workflows/publish_bridge.yml @@ -0,0 +1,73 @@ +name: Publish Bridge Package + +on: + push: + tags: + - "ask_ui_bridge-v[0-9]+.[0-9]+.[0-9]+" + +jobs: + publish: + name: Publish ask_ui_bridge to pub.dev + runs-on: ubuntu-latest + environment: pub.dev + permissions: + contents: read + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: apps/web/package-lock.json + + - name: Set up Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Verify tag matches bridge version + working-directory: apps/bridge + run: | + tag="${GITHUB_REF_NAME#ask_ui_bridge-v}" + version="$(sed -n 's/^version: //p' pubspec.yaml)" + test "$tag" = "$version" + + - name: Install web dependencies + working-directory: apps/web + run: npm ci --legacy-peer-deps + + - name: Build packaged Web workbench + working-directory: apps/web + run: npm run build:bridge + + - name: Verify packaged Web is committed + run: git diff --exit-code -- apps/bridge/web + + - name: Install bridge dependencies + working-directory: apps/bridge + run: dart pub get + + - name: Analyze bridge + working-directory: apps/bridge + run: dart analyze + + - name: Test bridge + working-directory: apps/bridge + run: dart test + + - name: Validate bridge package layout + working-directory: apps/bridge + run: dart tool/validate_release_layout.dart + + - name: Dry-run pub publish + working-directory: apps/bridge + run: dart pub publish --dry-run + + - name: Publish to pub.dev + working-directory: apps/bridge + run: dart pub publish --force diff --git a/.gitignore b/.gitignore index 39b8814..7e19186 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Miscellaneous *.class *.lock +!apps/bridge/pubspec.lock *.log *.pyc *.swp @@ -116,4 +117,4 @@ app.*.symbols !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock \ No newline at end of file +!/dev/ci/**/Gemfile.lock diff --git a/README.md b/README.md index 14cb9cd..f126e70 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Ask UI is a Flutter developer workbench for selecting precise UI targets, collec - `docs` - Product and user-facing documentation. - `docs_internal` - Internal implementation notes, issue plans, and progress notes. - `issues` - Issue tracking artifacts. +- `skills` - Coding-agent workflows for Ask UI. ## Bridge @@ -19,6 +20,69 @@ dart pub get dart run bin/ask_ui_bridge.dart --host 127.0.0.1 --port 8787 ``` +Installed Flutter projects should add `ask_ui_runtime`, register the runtime +before `runApp`, and start Ask UI through the globally activated bridge CLI: + +```yaml +dependencies: + ask_ui_runtime: ^0.0.1 +``` + +```dart +import 'package:ask_ui_runtime/ask_ui_runtime.dart'; + +void main() { + registerAskUiRuntime(); + runApp(const MyApp()); +} +``` + +```sh +dart pub global activate ask_ui_bridge +ask_ui_bridge launch +``` + +## Coding-Agent Skill + +Ask UI includes two launch workflow skills: + +- `skills/ask-ui/SKILL.md` for normal Flutter projects using the installed + `ask_ui_bridge` CLI. +- `skills/ask-ui-dev/SKILL.md` for Ask UI maintainers working from this source + checkout. + +The skills are intentionally installed manually in this version. They tell the +agent to start Ask UI, handle device selection, and write Ask UI Chat replies +through the returned `agent poll` command. + +Ask UI maintainers prepare the bridge package for release by building the Web +workbench into the bridge package before running pub validation: + +```sh +cd apps/web +npm run build:bridge + +cd ../bridge +dart run tool/validate_release_layout.dart +dart pub publish --dry-run +``` + +Formal bridge package publishing is handled by the +`Publish Bridge Package` GitHub Actions workflow. Configure pub.dev automated +publishing for this repository and the `ask_ui_bridge-v[0-9]+.[0-9]+.[0-9]+` +tag pattern, then publish by pushing a tag that matches the bridge package +version: + +```sh +git tag ask_ui_bridge-v0.0.4 +git push origin ask_ui_bridge-v0.0.4 +``` + +The publish workflow rebuilds the Web workbench, verifies the generated +`apps/bridge/web` files match the committed package files, runs bridge analysis +and tests, validates the release layout, performs `dart pub publish --dry-run`, +and only then runs `dart pub publish --force`. + Workbench sessions require `vmServiceUri`, `projectRoot`, and `deviceId` query parameters. `deviceId` must be the Android device or emulator serial used by Flutter, ADB, and scrcpy. diff --git a/apps/bridge/.pubignore b/apps/bridge/.pubignore new file mode 100644 index 0000000..04d8a54 --- /dev/null +++ b/apps/bridge/.pubignore @@ -0,0 +1,6 @@ +.dart_tool/ +build/ +coverage/ +test/ +*.log +.DS_Store diff --git a/apps/bridge/CHANGELOG.md b/apps/bridge/CHANGELOG.md new file mode 100644 index 0000000..9caab0b --- /dev/null +++ b/apps/bridge/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.0.4 + +- Add the `ask_ui_bridge` executable for `ask_ui_bridge launch` after global activation. +- Add the launch workflow that starts Flutter, creates a Bridge Session, opens + the workbench, and returns the Agent Session Command. +- Add `--web-dev` for Ask UI contributors who need the Vite workbench during + local development. +- Package the built Ask UI Web workbench under the bridge package `web/` + directory for installed users. +- Add bridge release layout validation for pub publishing. diff --git a/apps/bridge/LICENSE b/apps/bridge/LICENSE new file mode 100644 index 0000000..c5494cf --- /dev/null +++ b/apps/bridge/LICENSE @@ -0,0 +1,3 @@ +Copyright (c) 2026 Ask UI contributors. + +All rights reserved. diff --git a/apps/bridge/README.md b/apps/bridge/README.md new file mode 100644 index 0000000..4d270db --- /dev/null +++ b/apps/bridge/README.md @@ -0,0 +1,67 @@ +# Ask UI Bridge + +`ask_ui_bridge` is the local launcher and bridge server for Ask UI. It starts a +Flutter app, creates the Bridge Session used by the Ask UI workbench, serves the +packaged Web workbench, and exposes the Agent Session Command used by coding +agents. + +## Installed Usage + +Globally activate the bridge CLI: + +```sh +dart pub global activate ask_ui_bridge +``` + +Start Ask UI from a Flutter project that has registered `ask_ui_runtime`: + +```sh +ask_ui_bridge launch +``` + +Pass Flutter launch options when needed: + +```sh +ask_ui_bridge launch \ + --device \ + --flavor \ + --target lib/main_dev.dart \ + --dart-define API_BASE_URL=http://localhost:3000 \ + --project-root /path/to/flutter_app +``` + +Use `--no-open` when the command should print the workbench URL without opening +a browser. + +## Coding-Agent Skill + +The Ask UI repository includes launch workflow skills at `../../skills/ask-ui` +and `../../skills/ask-ui-dev`. Install `ask-ui` for normal Flutter projects, or +`ask-ui-dev` when developing this repository. The skills let Codex, Claude Code, +or a similar agent run the appropriate launch command, handle device selection, +run the returned Agent Session Command, and continue polling for Ask UI Chat +messages. + +This version does not include an automatic skill installer command. + +## Release Validation + +Maintainers should build the Web workbench into the bridge package before +publishing: + +```sh +cd apps/web +npm run build:bridge + +cd ../bridge +dart run tool/validate_release_layout.dart +dart test +dart pub publish --dry-run +``` + +The published package must include `web/index.html` and the generated Web +assets under `web/assets`. + +The repository publish workflow runs the same release validation for +`ask_ui_bridge-v` tags, verifies the packaged Web files are committed, +and publishes to pub.dev after `dart pub publish --dry-run` succeeds. diff --git a/apps/bridge/lib/device/scrcpy_device_stream.dart b/apps/bridge/lib/device/scrcpy_device_stream.dart index b3dd771..ecbc4e6 100644 --- a/apps/bridge/lib/device/scrcpy_device_stream.dart +++ b/apps/bridge/lib/device/scrcpy_device_stream.dart @@ -73,16 +73,40 @@ const _vendoredScrcpyServerPath = 'vendor/scrcpy/4.0/scrcpy-server-v4.0'; /// The bridge is commonly launched either from `apps/bridge` during local /// development or from the repository root by automation. Prefer the bridge /// package-local path when present and fall back to the repo-root path. -String defaultScrcpyServerPath({Directory? currentDirectory}) { +String defaultScrcpyServerPath({Directory? currentDirectory, Uri? scriptUri}) { final cwd = currentDirectory ?? Directory.current; - final bridgeLocalArtifact = File('${cwd.path}/$_vendoredScrcpyServerPath'); - if (bridgeLocalArtifact.existsSync()) { - return bridgeLocalArtifact.absolute.path; + final candidates = [ + ..._scrcpyServerCandidatesForScript(scriptUri ?? Platform.script), + File('${cwd.path}/$_vendoredScrcpyServerPath'), + File('${cwd.path}/apps/bridge/$_vendoredScrcpyServerPath'), + ]; + + for (final candidate in candidates) { + if (candidate.existsSync()) { + return candidate.absolute.path; + } + } + + return candidates.first.absolute.path; +} + +List _scrcpyServerCandidatesForScript(Uri scriptUri) { + if (scriptUri.scheme != 'file') { + return const []; + } + + final scriptFile = File.fromUri(scriptUri); + final scriptDirectory = scriptFile.parent; + final scriptDirectorySegments = + scriptDirectory.uri.pathSegments.where((segment) => segment.isNotEmpty); + if (scriptDirectorySegments.isEmpty || + scriptDirectorySegments.last != 'bin') { + return const []; } - return File('${cwd.path}/apps/bridge/$_vendoredScrcpyServerPath') - .absolute - .path; + return [ + File('${scriptDirectory.parent.path}/$_vendoredScrcpyServerPath'), + ]; } /// Result from a completed local command. diff --git a/apps/bridge/lib/launch/launch_adapters.dart b/apps/bridge/lib/launch/launch_adapters.dart new file mode 100644 index 0000000..9f9daee --- /dev/null +++ b/apps/bridge/lib/launch/launch_adapters.dart @@ -0,0 +1,303 @@ +part of 'launch_command.dart'; + +class _FlutterRunAppLauncher implements LaunchAppLauncher { + const _FlutterRunAppLauncher(); + + @override + Future launch({ + required List flutterRunArguments, + required String projectRoot, + }) async { + late final Process process; + try { + process = await Process.start( + 'flutter', + flutterRunArguments, + workingDirectory: projectRoot, + ); + } catch (_) { + throw const LaunchAppException('flutter_run_failed'); + } + + final Completer vmServiceCompleter = + Completer(); + final StringBuffer startupOutput = StringBuffer(); + + void observeOutput(List data) { + final String text = utf8.decode(data, allowMalformed: true); + startupOutput.write(text); + final String? vmServiceUri = + parseFlutterVmServiceUriFromMachineOutput(startupOutput.toString()) ?? + parseFlutterVmServiceUriFromOutput(startupOutput.toString()); + if (vmServiceUri != null && !vmServiceCompleter.isCompleted) { + vmServiceCompleter + .complete(LaunchAppResult(vmServiceUri: vmServiceUri)); + } + } + + process.stdout.listen(observeOutput); + process.stderr.listen(observeOutput); + process.exitCode.then((int exitCode) { + if (!vmServiceCompleter.isCompleted) { + vmServiceCompleter.completeError( + const LaunchAppException('flutter_run_failed'), + ); + } + }); + + try { + return await vmServiceCompleter.future.timeout( + const Duration(minutes: 2), + ); + } on LaunchAppException { + rethrow; + } on TimeoutException { + process.kill(); + throw const LaunchAppException('flutter_vm_service_not_found'); + } catch (_) { + throw const LaunchAppException('flutter_run_failed'); + } + } +} + +/// Extract the VM Service WebSocket URI from `flutter run --machine` output. +String? parseFlutterVmServiceUriFromMachineOutput(String output) { + for (final String line in const LineSplitter().convert(output)) { + final Object? decoded; + try { + decoded = jsonDecode(line); + } catch (_) { + continue; + } + final Map? message = _flutterMachineMessage(decoded); + if (message == null) { + continue; + } + + final Object? params = message['params']; + if (message['event'] == 'app.debugPort' && params is Map) { + final String? vmServiceUri = _stringValue(params['wsUri']) ?? + _stringValue(params['vmServiceUri']) ?? + _stringValue(params['uri']); + final String? normalized = _normalizeVmServiceUri(vmServiceUri); + if (normalized != null) { + return normalized; + } + } + + final Object? result = message['result']; + if (result is Map) { + final String? normalized = _normalizeVmServiceUri( + _stringValue(result['vmServiceUri']), + ); + if (normalized != null) { + return normalized; + } + } + } + + return null; +} + +Map? _flutterMachineMessage(Object? decoded) { + if (decoded is Map) { + return decoded; + } + if (decoded is List && decoded.length == 1) { + final Object? first = decoded.single; + if (first is Map) { + return first; + } + } + + return null; +} + +/// Extract the VM Service WebSocket URI from Flutter startup output. +/// +/// Flutter commonly prints an HTTP service URI ending in the auth-code path, +/// while the existing Bridge Session contract stores the WebSocket URI. Already +/// normalized `ws` and `wss` URIs are returned unchanged. +String? parseFlutterVmServiceUriFromOutput(String output) { + final RegExp uriPattern = RegExp( + r'(?:A Dart VM Service is available at:|The Dart VM Service is listening on)\s+' + r'((?:wss?|https?)://[^\s]+)', + ); + for (final RegExpMatch match in uriPattern.allMatches(output)) { + final String rawUri = match.group(1)!.replaceFirst(RegExp(r'[),.;]+$'), ''); + final Uri? uri = Uri.tryParse(rawUri); + if (uri == null || uri.host.isEmpty) { + continue; + } + final String? normalized = _normalizeVmServiceUri(uri.toString()); + if (normalized != null) { + return normalized; + } + } + + return null; +} + +String? _normalizeVmServiceUri(String? value) { + final Uri? uri = Uri.tryParse(value ?? ''); + if (uri == null || uri.host.isEmpty) { + return null; + } + if (uri.scheme == 'ws' || uri.scheme == 'wss') { + return uri.toString(); + } + if (uri.scheme == 'http' || uri.scheme == 'https') { + final String websocketScheme = uri.scheme == 'https' ? 'wss' : 'ws'; + final String normalizedPath = uri.path.endsWith('/ws') + ? uri.path + : '${uri.path.endsWith('/') ? uri.path : '${uri.path}/'}ws'; + return uri + .replace( + scheme: websocketScheme, + path: normalizedPath, + ) + .toString(); + } + + return null; +} + +String? _stringValue(Object? value) { + if (value is String && value.trim().isNotEmpty) { + return value.trim(); + } + + return null; +} + +/// Starts the local Bridge Server and posts the launch session request to it. +class LocalBridgeLauncher implements LaunchBridgeLauncher { + LocalBridgeLauncher({ + HttpClient? httpClient, + Directory? packagedWebRoot, + }) : _httpClient = httpClient ?? HttpClient(), + _packagedWebRoot = packagedWebRoot; + + final HttpClient _httpClient; + final Directory? _packagedWebRoot; + AskUiBridgeServer? _server; + + @override + Future createSession({ + required String vmServiceUri, + required String projectRoot, + required String deviceId, + required bool requirePackagedWeb, + }) async { + final Uri bridgeUrl = await _startOrReuseServer( + requirePackagedWeb: requirePackagedWeb, + ); + final Uri sessionUri = bridgeUrl.resolve('/api/sessions'); + + try { + final HttpClientRequest request = await _httpClient.postUrl(sessionUri); + request.headers.contentType = ContentType.json; + request.write( + jsonEncode({ + 'vmServiceUri': vmServiceUri, + 'projectRoot': projectRoot, + 'deviceId': deviceId, + }), + ); + final HttpClientResponse response = await request.close(); + final String responseBody = await utf8.decodeStream(response); + final Object? decoded = jsonDecode(responseBody); + if (decoded is! Map) { + throw const FormatException('Expected session JSON object'); + } + if (response.statusCode != HttpStatus.ok) { + throw const LaunchBridgeException('session_creation_failed'); + } + final Object? sessionId = decoded['sessionId']; + if (sessionId is! String || sessionId.trim().isEmpty) { + throw const LaunchBridgeException('session_creation_failed'); + } + return LaunchBridgeSession( + bridgeUrl: bridgeUrl, + sessionId: sessionId, + ); + } on LaunchBridgeException { + rethrow; + } catch (_) { + throw const LaunchBridgeException('session_creation_failed'); + } + } + + Future _startOrReuseServer({required bool requirePackagedWeb}) async { + final Uri bridgeUrl = Uri.parse('http://127.0.0.1:8787'); + if (_server != null) { + return bridgeUrl; + } + + final BridgeLogger logger = BridgeLogger(write: stderr.writeln); + final Directory packagedWebRoot = + _packagedWebRoot ?? await _resolvePackagedWebRoot(); + final bool hasPackagedWeb = + await File('${packagedWebRoot.path}/index.html').exists(); + if (requirePackagedWeb && !hasPackagedWeb) { + throw const LaunchBridgeException('packaged_web_not_found'); + } + final AskUiBridgeServer server = AskUiBridgeServer( + sessionStore: SessionStore(), + inspectorClient: VmServiceFlutterInspectorClient( + vmServiceFactory: VmServiceFactory(), + ), + appController: VmServiceFlutterAppController( + vmServiceFactory: VmServiceFactory(), + logger: logger, + ), + packagedWebRoot: hasPackagedWeb ? packagedWebRoot : null, + logger: logger, + ); + + try { + await server.start(host: '127.0.0.1', port: 8787); + _server = server; + return bridgeUrl; + } on SocketException { + return bridgeUrl; + } catch (_) { + throw const LaunchBridgeException('bridge_start_failed'); + } + } +} + +class _PlatformBrowserOpener implements LaunchBrowserOpener { + const _PlatformBrowserOpener(); + + @override + Future open(Uri url) async { + final List command = _browserOpenCommand(url); + try { + await Process.start(command.first, command.sublist(1)); + } catch (_) { + throw const LaunchBrowserOpenException('browser_open_failed'); + } + } + + List _browserOpenCommand(Uri url) { + if (Platform.isMacOS) { + return ['open', url.toString()]; + } + if (Platform.isWindows) { + return ['cmd', '/c', 'start', '', url.toString()]; + } + return ['xdg-open', url.toString()]; + } +} + +Future _resolvePackagedWebRoot() async { + final Uri? serverLibraryUri = await Isolate.resolvePackageUri( + Uri.parse('package:ask_ui_bridge/server/ask_ui_bridge_server.dart'), + ); + if (serverLibraryUri == null) { + return Directory('web'); + } + + return Directory.fromUri(serverLibraryUri.resolve('../../web')); +} diff --git a/apps/bridge/lib/launch/launch_command.dart b/apps/bridge/lib/launch/launch_command.dart index 3df971b..ca18589 100644 --- a/apps/bridge/lib/launch/launch_command.dart +++ b/apps/bridge/lib/launch/launch_command.dart @@ -9,99 +9,26 @@ import '../logging/bridge_logger.dart'; import '../server/ask_ui_bridge_server.dart'; import '../sessions/session_store.dart'; -typedef FlutterDevicesRunner = Future Function( - String executable, - List arguments, -); - -/// Starts the target Flutter app and returns the VM Service URI it exposes. -abstract interface class LaunchAppLauncher { - Future launch({ - required List flutterRunArguments, - required String projectRoot, - }); -} - -/// VM Service details captured from a successful Flutter app startup. -class LaunchAppResult { - const LaunchAppResult({required this.vmServiceUri}); - - final String vmServiceUri; -} - -/// Normalized Flutter startup failure for launch command JSON output. -class LaunchAppException implements Exception { - const LaunchAppException(this.code); - - final String code; -} - -/// Starts or reuses the local Bridge Server and creates a Bridge Session. -abstract interface class LaunchBridgeLauncher { - Future createSession({ - required String vmServiceUri, - required String projectRoot, - required String deviceId, - }); -} - -/// Bridge Session connection details returned to the launcher. -class LaunchBridgeSession { - const LaunchBridgeSession({ - required this.bridgeUrl, - required this.sessionId, - }); - - final Uri bridgeUrl; - final String sessionId; -} - -/// Normalized Bridge startup/session failure for launch command JSON output. -class LaunchBridgeException implements Exception { - const LaunchBridgeException(this.code); - - final String code; -} - -/// Opens the packaged workbench URL after a launch creates a Bridge Session. -abstract interface class LaunchBrowserOpener { - Future open(Uri url); -} - -/// Normalized browser-open failure recorded in successful launch output. -class LaunchBrowserOpenException implements Exception { - const LaunchBrowserOpenException(this.code); - - final String code; -} - -/// Result returned by the launch command runner. -/// -/// The binary writes `stdout`, `stderr`, and `exitCode` directly. Tests use the -/// same object to verify the JSON contract without spawning a subprocess. -class LaunchCommandResult { - const LaunchCommandResult({ - required this.exitCode, - this.stdout = '', - this.stderr = '', - }); - - final int exitCode; - final String stdout; - final String stderr; -} +part 'launch_adapters.dart'; +part 'launch_contracts.dart'; +part 'launch_devices.dart'; +part 'launch_options.dart'; +part 'launch_output.dart'; +part 'launch_web_dev.dart'; +part 'launch_workbench_url.dart'; /// Runs the Ask UI launch command and returns machine-readable JSON. /// /// The command selects a usable Flutter device, starts the app, creates a -/// Bridge Session, builds the packaged workbench attach URL, and opens it -/// unless `--no-open` is present. +/// Bridge Session, builds the packaged or Web dev workbench attach URL, and +/// opens it unless `--no-open` is present. Future runLaunchCommand( List args, { FlutterDevicesRunner listDevices = Process.run, LaunchAppLauncher? appLauncher, LaunchBridgeLauncher? bridgeLauncher, LaunchBrowserOpener? browserOpener, + LaunchWebDevServer? webDevServer, }) async { late final _LaunchOptions options; try { @@ -132,8 +59,9 @@ Future runLaunchCommand( options: options, selectedDevice: matchingDevices.single, appLauncher: appLauncher ?? const _FlutterRunAppLauncher(), - bridgeLauncher: bridgeLauncher ?? _LocalBridgeLauncher(), + bridgeLauncher: bridgeLauncher ?? LocalBridgeLauncher(), browserOpener: browserOpener ?? const _PlatformBrowserOpener(), + webDevServer: webDevServer ?? NpmViteWebDevServer(), ); } @@ -146,8 +74,9 @@ Future runLaunchCommand( options: options, selectedDevice: usableDevices.single, appLauncher: appLauncher ?? const _FlutterRunAppLauncher(), - bridgeLauncher: bridgeLauncher ?? _LocalBridgeLauncher(), + bridgeLauncher: bridgeLauncher ?? LocalBridgeLauncher(), browserOpener: browserOpener ?? const _PlatformBrowserOpener(), + webDevServer: webDevServer ?? NpmViteWebDevServer(), ); } @@ -163,6 +92,7 @@ Future _launchSelectedDevice({ required LaunchAppLauncher appLauncher, required LaunchBridgeLauncher bridgeLauncher, required LaunchBrowserOpener browserOpener, + required LaunchWebDevServer webDevServer, }) async { final String projectRoot = options.projectRoot ?? Directory.current.absolute.path; @@ -182,12 +112,25 @@ Future _launchSelectedDevice({ vmServiceUri: appResult.vmServiceUri, projectRoot: projectRoot, deviceId: selectedDevice.id, + requirePackagedWeb: !options.webDev, ); } on LaunchBridgeException catch (error) { return _LaunchOutput.failure(error.code); } + late final Uri workbenchBaseUrl; + if (options.webDev) { + try { + workbenchBaseUrl = await webDevServer.start(projectRoot: projectRoot); + } on LaunchWebDevException catch (error) { + return _LaunchOutput.failure(error.code); + } + } else { + workbenchBaseUrl = bridgeSession.bridgeUrl; + } + final Uri workbenchUrl = buildLaunchWorkbenchUrl( + workbenchBaseUrl: workbenchBaseUrl, bridgeUrl: bridgeSession.bridgeUrl, sessionId: bridgeSession.sessionId, selectedDeviceId: selectedDevice.id, @@ -219,591 +162,3 @@ Future _launchSelectedDevice({ browserOpenError: browserOpenError, ); } - -/// Build the packaged workbench URL that attaches to an existing session. -/// -/// The bridge server serves the React workbench from `/`, while the query -/// parameters select Web attach mode with the already-created Bridge Session. -Uri buildLaunchWorkbenchUrl({ - required Uri bridgeUrl, - required String sessionId, - required String selectedDeviceId, - required String projectRoot, - required String? flavor, - required String? target, -}) { - final Map queryParameters = { - 'bridgeUrl': _withoutTrailingSlash(bridgeUrl.toString()), - 'sessionId': sessionId, - 'deviceId': selectedDeviceId, - 'projectRoot': projectRoot, - }; - if (flavor != null) { - queryParameters['flavor'] = flavor; - } - if (target != null) { - queryParameters['target'] = target; - } - - return bridgeUrl.replace( - path: '/', - queryParameters: queryParameters, - ); -} - -List<_LaunchDevice> _matchingDevices( - List<_LaunchDevice> usableDevices, - String? requestedDevice, -) { - final String? trimmedRequest = requestedDevice?.trim(); - if (trimmedRequest == null || trimmedRequest.isEmpty) { - return const <_LaunchDevice>[]; - } - - final List<_LaunchDevice> idMatches = usableDevices - .where((device) => device.id == trimmedRequest) - .toList(growable: false); - if (idMatches.isNotEmpty) { - return idMatches; - } - - final String normalizedRequest = trimmedRequest.toLowerCase(); - return usableDevices - .where( - (device) => device.name.toLowerCase().contains(normalizedRequest), - ) - .toList(growable: false); -} - -class _LaunchOptions { - const _LaunchOptions({ - required this.requestedDevice, - required this.flavor, - required this.target, - required this.dartDefines, - required this.projectRoot, - required this.open, - }); - - final String? requestedDevice; - final String? flavor; - final String? target; - final List dartDefines; - final String? projectRoot; - final bool open; - - static _LaunchOptions parse(List args) { - if (args.isEmpty || args.first != 'launch') { - throw const _LaunchValidationError(); - } - - String? requestedDevice; - String? flavor; - String? target; - final List dartDefines = []; - String? projectRoot; - bool open = true; - - for (var index = 1; index < args.length; index += 1) { - final String arg = args[index]; - if (arg == '--device' && index + 1 < args.length) { - index += 1; - requestedDevice = args[index]; - } else if (arg == '--flavor' && index + 1 < args.length) { - index += 1; - flavor = args[index]; - } else if (arg == '--target' && index + 1 < args.length) { - index += 1; - target = args[index]; - } else if (arg == '--dart-define' && index + 1 < args.length) { - index += 1; - dartDefines.add(args[index]); - } else if (arg == '--project-root' && index + 1 < args.length) { - index += 1; - projectRoot = args[index]; - } else if (arg == '--no-open') { - open = false; - } else { - throw const _LaunchValidationError(); - } - } - - return _LaunchOptions( - requestedDevice: _emptyToNull(requestedDevice), - flavor: _emptyToNull(flavor), - target: _emptyToNull(target), - dartDefines: List.unmodifiable(dartDefines), - projectRoot: _emptyToNull(projectRoot), - open: open, - ); - } - - Map toJson() { - return { - 'device': requestedDevice, - 'flavor': flavor, - 'target': target, - 'dartDefines': dartDefines, - 'projectRoot': projectRoot, - 'open': open, - }; - } - - List flutterRunArguments(String deviceId) { - final List arguments = [ - 'run', - '--device-id', - deviceId, - ]; - - if (flavor != null) { - arguments.addAll(['--flavor', flavor!]); - } - if (target != null) { - arguments.addAll(['--target', target!]); - } - for (final String dartDefine in dartDefines) { - arguments.addAll(['--dart-define', dartDefine]); - } - - return arguments; - } - - List rerunArguments(String deviceId) { - final List arguments = [ - 'dart', - 'run', - 'ask_ui_bridge', - 'launch', - '--device', - deviceId, - ]; - - if (flavor != null) { - arguments.addAll(['--flavor', flavor!]); - } - if (target != null) { - arguments.addAll(['--target', target!]); - } - for (final String dartDefine in dartDefines) { - arguments.addAll(['--dart-define', dartDefine]); - } - if (projectRoot != null) { - arguments.addAll(['--project-root', projectRoot!]); - } - if (!open) { - arguments.add('--no-open'); - } - - return arguments; - } -} - -class _FlutterDeviceDiscovery { - const _FlutterDeviceDiscovery({ - required this.listDevices, - }); - - final FlutterDevicesRunner listDevices; - - Future> discoverUsableDevices() async { - final ProcessResult result = await listDevices( - 'flutter', - const ['devices', '--machine'], - ); - if (result.exitCode != 0) { - throw const _DeviceDiscoveryException(); - } - - late final Object? decoded; - try { - decoded = jsonDecode(result.stdout.toString()); - } on FormatException { - throw const _DeviceDiscoveryException(); - } - - if (decoded is! List) { - throw const _DeviceDiscoveryException(); - } - - final List<_LaunchDevice> devices = <_LaunchDevice>[]; - for (final Object? rawDevice in decoded) { - final _LaunchDevice? device = _LaunchDevice.fromJson(rawDevice); - if (device != null && device.isUsable) { - devices.add(device); - } - } - - return List<_LaunchDevice>.unmodifiable(devices); - } -} - -class _FlutterRunAppLauncher implements LaunchAppLauncher { - const _FlutterRunAppLauncher(); - - @override - Future launch({ - required List flutterRunArguments, - required String projectRoot, - }) async { - late final Process process; - try { - process = await Process.start( - 'flutter', - flutterRunArguments, - workingDirectory: projectRoot, - ); - } catch (_) { - throw const LaunchAppException('flutter_run_failed'); - } - - final Completer vmServiceCompleter = - Completer(); - final StringBuffer startupOutput = StringBuffer(); - - void observeOutput(List data) { - final String text = utf8.decode(data, allowMalformed: true); - startupOutput.write(text); - final String? vmServiceUri = - parseFlutterVmServiceUriFromOutput(startupOutput.toString()); - if (vmServiceUri != null && !vmServiceCompleter.isCompleted) { - vmServiceCompleter - .complete(LaunchAppResult(vmServiceUri: vmServiceUri)); - } - } - - process.stdout.listen(observeOutput); - process.stderr.listen(observeOutput); - process.exitCode.then((int exitCode) { - if (!vmServiceCompleter.isCompleted) { - vmServiceCompleter.completeError( - const LaunchAppException('flutter_run_failed'), - ); - } - }); - - try { - return await vmServiceCompleter.future.timeout( - const Duration(minutes: 2), - ); - } on LaunchAppException { - rethrow; - } on TimeoutException { - process.kill(); - throw const LaunchAppException('flutter_vm_service_not_found'); - } catch (_) { - throw const LaunchAppException('flutter_run_failed'); - } - } -} - -/// Extract the VM Service WebSocket URI from Flutter startup output. -/// -/// Flutter commonly prints an HTTP service URI ending in the auth-code path, -/// while the existing Bridge Session contract stores the WebSocket URI. Already -/// normalized `ws` and `wss` URIs are returned unchanged. -String? parseFlutterVmServiceUriFromOutput(String output) { - final RegExp uriPattern = RegExp(r'(wss?|https?)://[^\s]+'); - for (final RegExpMatch match in uriPattern.allMatches(output)) { - final String rawUri = match.group(0)!.replaceFirst(RegExp(r'[),.;]+$'), ''); - final Uri? uri = Uri.tryParse(rawUri); - if (uri == null || uri.host.isEmpty) { - continue; - } - if (uri.scheme == 'ws' || uri.scheme == 'wss') { - return uri.toString(); - } - if (uri.scheme == 'http' || uri.scheme == 'https') { - final String websocketScheme = uri.scheme == 'https' ? 'wss' : 'ws'; - final String normalizedPath = uri.path.endsWith('/ws') - ? uri.path - : '${uri.path.endsWith('/') ? uri.path : '${uri.path}/'}ws'; - return uri - .replace( - scheme: websocketScheme, - path: normalizedPath, - ) - .toString(); - } - } - - return null; -} - -class _LocalBridgeLauncher implements LaunchBridgeLauncher { - _LocalBridgeLauncher({HttpClient? httpClient}) - : _httpClient = httpClient ?? HttpClient(); - - final HttpClient _httpClient; - AskUiBridgeServer? _server; - - @override - Future createSession({ - required String vmServiceUri, - required String projectRoot, - required String deviceId, - }) async { - final Uri bridgeUrl = await _startOrReuseServer(); - final Uri sessionUri = bridgeUrl.resolve('/api/sessions'); - - try { - final HttpClientRequest request = await _httpClient.postUrl(sessionUri); - request.headers.contentType = ContentType.json; - request.write( - jsonEncode({ - 'vmServiceUri': vmServiceUri, - 'projectRoot': projectRoot, - 'deviceId': deviceId, - }), - ); - final HttpClientResponse response = await request.close(); - final String responseBody = await utf8.decodeStream(response); - final Object? decoded = jsonDecode(responseBody); - if (decoded is! Map) { - throw const FormatException('Expected session JSON object'); - } - if (response.statusCode != HttpStatus.ok) { - throw const LaunchBridgeException('session_creation_failed'); - } - final Object? sessionId = decoded['sessionId']; - if (sessionId is! String || sessionId.trim().isEmpty) { - throw const LaunchBridgeException('session_creation_failed'); - } - return LaunchBridgeSession( - bridgeUrl: bridgeUrl, - sessionId: sessionId, - ); - } on LaunchBridgeException { - rethrow; - } catch (_) { - throw const LaunchBridgeException('session_creation_failed'); - } - } - - Future _startOrReuseServer() async { - final Uri bridgeUrl = Uri.parse('http://127.0.0.1:8787'); - if (_server != null) { - return bridgeUrl; - } - - final BridgeLogger logger = BridgeLogger(write: stderr.writeln); - final Directory packagedWebRoot = await _resolvePackagedWebRoot(); - final AskUiBridgeServer server = AskUiBridgeServer( - sessionStore: SessionStore(), - inspectorClient: VmServiceFlutterInspectorClient( - vmServiceFactory: VmServiceFactory(), - ), - appController: VmServiceFlutterAppController( - vmServiceFactory: VmServiceFactory(), - logger: logger, - ), - packagedWebRoot: packagedWebRoot, - logger: logger, - ); - - try { - await server.start(host: '127.0.0.1', port: 8787); - _server = server; - return bridgeUrl; - } on SocketException { - return bridgeUrl; - } catch (_) { - throw const LaunchBridgeException('bridge_start_failed'); - } - } -} - -class _PlatformBrowserOpener implements LaunchBrowserOpener { - const _PlatformBrowserOpener(); - - @override - Future open(Uri url) async { - final List command = _browserOpenCommand(url); - try { - await Process.start(command.first, command.sublist(1)); - } catch (_) { - throw const LaunchBrowserOpenException('browser_open_failed'); - } - } - - List _browserOpenCommand(Uri url) { - if (Platform.isMacOS) { - return ['open', url.toString()]; - } - if (Platform.isWindows) { - return ['cmd', '/c', 'start', '', url.toString()]; - } - return ['xdg-open', url.toString()]; - } -} - -class _LaunchDevice { - const _LaunchDevice({ - required this.id, - required this.name, - required this.targetPlatform, - required this.isSupported, - }); - - final String id; - final String name; - final String targetPlatform; - final bool isSupported; - - bool get isUsable { - return isSupported && targetPlatform.toLowerCase().startsWith('android'); - } - - static _LaunchDevice? fromJson(Object? rawDevice) { - if (rawDevice is! Map) { - return null; - } - - final Object? rawId = rawDevice['id']; - final Object? rawTargetPlatform = rawDevice['targetPlatform']; - if (rawId is! String || rawTargetPlatform is! String) { - return null; - } - - final Object? rawName = rawDevice['name']; - return _LaunchDevice( - id: rawId, - name: rawName is String ? rawName : rawId, - targetPlatform: rawTargetPlatform, - isSupported: rawDevice['isSupported'] != false, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'targetPlatform': targetPlatform, - }; - } -} - -class _LaunchOutput { - _LaunchOutput._(); - - static LaunchCommandResult ready({ - required _LaunchOptions options, - required _LaunchDevice selectedDevice, - required LaunchAppResult appResult, - required LaunchBridgeSession bridgeSession, - required String projectRoot, - required Uri workbenchUrl, - required bool browserOpened, - required String? browserOpenError, - }) { - final String agentCommand = _commandString([ - 'dart', - 'run', - 'ask_ui_bridge', - 'agent', - 'poll', - '--base-url', - bridgeSession.bridgeUrl.toString(), - '--session-id', - bridgeSession.sessionId, - ]); - return LaunchCommandResult( - exitCode: 0, - stdout: jsonEncode({ - 'status': 'ready', - 'selectedDevice': selectedDevice.toJson(), - 'launchIntent': options.toJson(), - 'bridgeUrl': bridgeSession.bridgeUrl.toString(), - 'sessionId': bridgeSession.sessionId, - 'vmServiceUri': appResult.vmServiceUri, - 'projectRoot': projectRoot, - 'flavor': options.flavor, - 'target': options.target, - 'agentCommand': agentCommand, - 'workbenchUrl': workbenchUrl.toString(), - 'browserOpened': browserOpened, - if (browserOpenError != null) 'browserOpenError': browserOpenError, - 'nextStep': 'Run the returned agent poll command.', - }), - ); - } - - static LaunchCommandResult needsDeviceSelection( - _LaunchOptions options, - List<_LaunchDevice> devices, - ) { - return LaunchCommandResult( - exitCode: 0, - stdout: jsonEncode({ - 'status': 'needs_device_selection', - 'devices': devices - .map((device) => { - ...device.toJson(), - 'suggestedCommand': _commandString( - options.rerunArguments(device.id), - ), - }) - .toList(growable: false), - 'launchIntent': options.toJson(), - 'nextStep': - 'Ask the user to choose a device, then rerun launch with --device.', - }), - ); - } - - static LaunchCommandResult failure(String code) { - return LaunchCommandResult( - exitCode: 1, - stderr: jsonEncode({ - 'status': 'error', - 'error': code, - }), - ); - } -} - -String _commandString(List arguments) { - return arguments.map(_shellQuote).join(' '); -} - - - -String _shellQuote(String argument) { - if (RegExp(r'^[A-Za-z0-9_./:=@+-]+$').hasMatch(argument)) { - return argument; - } - - return "'${argument.replaceAll("'", r"'\''")}'"; -} - -String? _emptyToNull(String? value) { - final String? trimmed = value?.trim(); - if (trimmed == null || trimmed.isEmpty) { - return null; - } - - return trimmed; -} - -String _withoutTrailingSlash(String value) { - return value.replaceFirst(RegExp(r'/+$'), ''); -} - -class _LaunchValidationError implements Exception { - const _LaunchValidationError(); -} - -class _DeviceDiscoveryException implements Exception { - const _DeviceDiscoveryException(); -} - -Future _resolvePackagedWebRoot() async { - final Uri? serverLibraryUri = await Isolate.resolvePackageUri( - Uri.parse('package:ask_ui_bridge/server/ask_ui_bridge_server.dart'), - ); - if (serverLibraryUri == null) { - return Directory('web'); - } - - return Directory.fromUri(serverLibraryUri.resolve('../../web')); -} diff --git a/apps/bridge/lib/launch/launch_contracts.dart b/apps/bridge/lib/launch/launch_contracts.dart new file mode 100644 index 0000000..e86f54d --- /dev/null +++ b/apps/bridge/lib/launch/launch_contracts.dart @@ -0,0 +1,102 @@ +part of 'launch_command.dart'; + +typedef FlutterDevicesRunner = Future Function( + String executable, + List arguments, +); + +typedef ViteDevServerProcessStarter = Future Function( + String executable, + List arguments, { + String? workingDirectory, +}); + +/// Starts the target Flutter app and returns the VM Service URI it exposes. +abstract interface class LaunchAppLauncher { + Future launch({ + required List flutterRunArguments, + required String projectRoot, + }); +} + +/// VM Service details captured from a successful Flutter app startup. +class LaunchAppResult { + const LaunchAppResult({required this.vmServiceUri}); + + final String vmServiceUri; +} + +/// Normalized Flutter startup failure for launch command JSON output. +class LaunchAppException implements Exception { + const LaunchAppException(this.code); + + final String code; +} + +/// Starts or reuses the local Bridge Server and creates a Bridge Session. +abstract interface class LaunchBridgeLauncher { + Future createSession({ + required String vmServiceUri, + required String projectRoot, + required String deviceId, + required bool requirePackagedWeb, + }); +} + +/// Bridge Session connection details returned to the launcher. +class LaunchBridgeSession { + const LaunchBridgeSession({ + required this.bridgeUrl, + required this.sessionId, + }); + + final Uri bridgeUrl; + final String sessionId; +} + +/// Normalized Bridge startup/session failure for launch command JSON output. +class LaunchBridgeException implements Exception { + const LaunchBridgeException(this.code); + + final String code; +} + +/// Opens the workbench URL after launch creates a Bridge Session. +abstract interface class LaunchBrowserOpener { + Future open(Uri url); +} + +/// Normalized browser-open failure recorded in successful launch output. +class LaunchBrowserOpenException implements Exception { + const LaunchBrowserOpenException(this.code); + + final String code; +} + +/// Starts or reuses the Vite workbench server for monorepo development. +abstract interface class LaunchWebDevServer { + Future start({required String projectRoot}); +} + +/// Normalized Web development server failure for launch command JSON output. +class LaunchWebDevException implements Exception { + const LaunchWebDevException(this.code); + + final String code; +} + +/// Result returned by the launch command runner. +/// +/// The binary writes `stdout`, `stderr`, and `exitCode` directly. Tests use the +/// same object to verify the JSON contract without spawning a subprocess. +class LaunchCommandResult { + const LaunchCommandResult({ + required this.exitCode, + this.stdout = '', + this.stderr = '', + }); + + final int exitCode; + final String stdout; + final String stderr; +} diff --git a/apps/bridge/lib/launch/launch_devices.dart b/apps/bridge/lib/launch/launch_devices.dart new file mode 100644 index 0000000..a57aab0 --- /dev/null +++ b/apps/bridge/lib/launch/launch_devices.dart @@ -0,0 +1,114 @@ +part of 'launch_command.dart'; + +class _FlutterDeviceDiscovery { + const _FlutterDeviceDiscovery({ + required this.listDevices, + }); + + final FlutterDevicesRunner listDevices; + + Future> discoverUsableDevices() async { + final ProcessResult result = await listDevices( + 'flutter', + const ['devices', '--machine'], + ); + if (result.exitCode != 0) { + throw const _DeviceDiscoveryException(); + } + + late final Object? decoded; + try { + decoded = jsonDecode(result.stdout.toString()); + } on FormatException { + throw const _DeviceDiscoveryException(); + } + + if (decoded is! List) { + throw const _DeviceDiscoveryException(); + } + + final List<_LaunchDevice> devices = <_LaunchDevice>[]; + for (final Object? rawDevice in decoded) { + final _LaunchDevice? device = _LaunchDevice.fromJson(rawDevice); + if (device != null && device.isUsable) { + devices.add(device); + } + } + + return List<_LaunchDevice>.unmodifiable(devices); + } +} + +List<_LaunchDevice> _matchingDevices( + List<_LaunchDevice> usableDevices, + String? requestedDevice, +) { + final String? trimmedRequest = requestedDevice?.trim(); + if (trimmedRequest == null || trimmedRequest.isEmpty) { + return const <_LaunchDevice>[]; + } + + final List<_LaunchDevice> idMatches = usableDevices + .where((device) => device.id == trimmedRequest) + .toList(growable: false); + if (idMatches.isNotEmpty) { + return idMatches; + } + + final String normalizedRequest = trimmedRequest.toLowerCase(); + return usableDevices + .where( + (device) => device.name.toLowerCase().contains(normalizedRequest), + ) + .toList(growable: false); +} + +class _LaunchDevice { + const _LaunchDevice({ + required this.id, + required this.name, + required this.targetPlatform, + required this.isSupported, + }); + + final String id; + final String name; + final String targetPlatform; + final bool isSupported; + + bool get isUsable { + return isSupported && targetPlatform.toLowerCase().startsWith('android'); + } + + static _LaunchDevice? fromJson(Object? rawDevice) { + if (rawDevice is! Map) { + return null; + } + + final Object? rawId = rawDevice['id']; + final Object? rawTargetPlatform = rawDevice['targetPlatform']; + if (rawId is! String || rawTargetPlatform is! String) { + return null; + } + + final Object? rawName = rawDevice['name']; + return _LaunchDevice( + id: rawId, + name: rawName is String ? rawName : rawId, + targetPlatform: rawTargetPlatform, + isSupported: rawDevice['isSupported'] != false, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'targetPlatform': targetPlatform, + }; + } +} + +class _DeviceDiscoveryException implements Exception { + const _DeviceDiscoveryException(); +} diff --git a/apps/bridge/lib/launch/launch_options.dart b/apps/bridge/lib/launch/launch_options.dart new file mode 100644 index 0000000..d99badd --- /dev/null +++ b/apps/bridge/lib/launch/launch_options.dart @@ -0,0 +1,147 @@ +part of 'launch_command.dart'; + +class _LaunchOptions { + const _LaunchOptions({ + required this.requestedDevice, + required this.flavor, + required this.target, + required this.dartDefines, + required this.projectRoot, + required this.open, + required this.webDev, + }); + + final String? requestedDevice; + final String? flavor; + final String? target; + final List dartDefines; + final String? projectRoot; + final bool open; + final bool webDev; + + static _LaunchOptions parse(List args) { + if (args.isEmpty || args.first != 'launch') { + throw const _LaunchValidationError(); + } + + String? requestedDevice; + String? flavor; + String? target; + final List dartDefines = []; + String? projectRoot; + bool open = true; + bool webDev = false; + + for (var index = 1; index < args.length; index += 1) { + final String arg = args[index]; + if (arg == '--device' && index + 1 < args.length) { + index += 1; + requestedDevice = args[index]; + } else if (arg == '--flavor' && index + 1 < args.length) { + index += 1; + flavor = args[index]; + } else if (arg == '--target' && index + 1 < args.length) { + index += 1; + target = args[index]; + } else if (arg == '--dart-define' && index + 1 < args.length) { + index += 1; + dartDefines.add(args[index]); + } else if (arg == '--project-root' && index + 1 < args.length) { + index += 1; + projectRoot = args[index]; + } else if (arg == '--no-open') { + open = false; + } else if (arg == '--web-dev') { + webDev = true; + } else { + throw const _LaunchValidationError(); + } + } + + return _LaunchOptions( + requestedDevice: _emptyToNull(requestedDevice), + flavor: _emptyToNull(flavor), + target: _emptyToNull(target), + dartDefines: List.unmodifiable(dartDefines), + projectRoot: _emptyToNull(projectRoot), + open: open, + webDev: webDev, + ); + } + + Map toJson() { + return { + 'device': requestedDevice, + 'flavor': flavor, + 'target': target, + 'dartDefines': dartDefines, + 'projectRoot': projectRoot, + 'open': open, + 'webDev': webDev, + }; + } + + List flutterRunArguments(String deviceId) { + final List arguments = [ + 'run', + '--machine', + '--device-id', + deviceId, + ]; + + if (flavor != null) { + arguments.addAll(['--flavor', flavor!]); + } + if (target != null) { + arguments.addAll(['--target', target!]); + } + for (final String dartDefine in dartDefines) { + arguments.addAll(['--dart-define', dartDefine]); + } + + return arguments; + } + + List rerunArguments(String deviceId) { + final List arguments = [ + 'ask_ui_bridge', + 'launch', + '--device', + deviceId, + ]; + + if (flavor != null) { + arguments.addAll(['--flavor', flavor!]); + } + if (target != null) { + arguments.addAll(['--target', target!]); + } + for (final String dartDefine in dartDefines) { + arguments.addAll(['--dart-define', dartDefine]); + } + if (projectRoot != null) { + arguments.addAll(['--project-root', projectRoot!]); + } + if (!open) { + arguments.add('--no-open'); + } + if (webDev) { + arguments.add('--web-dev'); + } + + return arguments; + } +} + +String? _emptyToNull(String? value) { + final String? trimmed = value?.trim(); + if (trimmed == null || trimmed.isEmpty) { + return null; + } + + return trimmed; +} + +class _LaunchValidationError implements Exception { + const _LaunchValidationError(); +} diff --git a/apps/bridge/lib/launch/launch_output.dart b/apps/bridge/lib/launch/launch_output.dart new file mode 100644 index 0000000..5935035 --- /dev/null +++ b/apps/bridge/lib/launch/launch_output.dart @@ -0,0 +1,90 @@ +part of 'launch_command.dart'; + +class _LaunchOutput { + _LaunchOutput._(); + + static LaunchCommandResult ready({ + required _LaunchOptions options, + required _LaunchDevice selectedDevice, + required LaunchAppResult appResult, + required LaunchBridgeSession bridgeSession, + required String projectRoot, + required Uri workbenchUrl, + required bool browserOpened, + required String? browserOpenError, + }) { + final String agentCommand = _commandString([ + 'ask_ui_bridge', + 'agent', + 'poll', + '--base-url', + bridgeSession.bridgeUrl.toString(), + '--session-id', + bridgeSession.sessionId, + ]); + return LaunchCommandResult( + exitCode: 0, + stdout: jsonEncode({ + 'status': 'ready', + 'selectedDevice': selectedDevice.toJson(), + 'launchIntent': options.toJson(), + 'bridgeUrl': bridgeSession.bridgeUrl.toString(), + 'sessionId': bridgeSession.sessionId, + 'vmServiceUri': appResult.vmServiceUri, + 'projectRoot': projectRoot, + 'flavor': options.flavor, + 'target': options.target, + 'agentCommand': agentCommand, + 'workbenchUrl': workbenchUrl.toString(), + 'browserOpened': browserOpened, + if (browserOpenError != null) 'browserOpenError': browserOpenError, + 'nextStep': 'Run the returned agent poll command.', + }), + ); + } + + static LaunchCommandResult needsDeviceSelection( + _LaunchOptions options, + List<_LaunchDevice> devices, + ) { + return LaunchCommandResult( + exitCode: 0, + stdout: jsonEncode({ + 'status': 'needs_device_selection', + 'devices': devices + .map((device) => { + ...device.toJson(), + 'suggestedCommand': _commandString( + options.rerunArguments(device.id), + ), + }) + .toList(growable: false), + 'launchIntent': options.toJson(), + 'nextStep': + 'Ask the user to choose a device, then rerun launch with --device.', + }), + ); + } + + static LaunchCommandResult failure(String code) { + return LaunchCommandResult( + exitCode: 1, + stderr: jsonEncode({ + 'status': 'error', + 'error': code, + }), + ); + } +} + +String _commandString(List arguments) { + return arguments.map(_shellQuote).join(' '); +} + +String _shellQuote(String argument) { + if (RegExp(r'^[A-Za-z0-9_./:=@+-]+$').hasMatch(argument)) { + return argument; + } + + return "'${argument.replaceAll("'", r"'\''")}'"; +} diff --git a/apps/bridge/lib/launch/launch_web_dev.dart b/apps/bridge/lib/launch/launch_web_dev.dart new file mode 100644 index 0000000..3c690ce --- /dev/null +++ b/apps/bridge/lib/launch/launch_web_dev.dart @@ -0,0 +1,95 @@ +part of 'launch_command.dart'; + +/// Extract the browser URL printed by Vite for the local dev server. +/// +/// Vite may choose an alternate port when the default is busy. The launch +/// command uses the printed `Local:` URL so the browser opens the actual server. +Uri? parseViteDevServerUrlFromOutput(String output) { + final RegExp localUrlPattern = RegExp( + r'Local:\s+(https?://[^\s]+)', + multiLine: true, + ); + final RegExpMatch? match = localUrlPattern.firstMatch(output); + if (match == null) { + return null; + } + + final Uri? uri = Uri.tryParse(match.group(1)!); + if (uri == null || uri.host.isEmpty) { + return null; + } + + return uri; +} + +/// Starts the monorepo React/Vite workbench used by Ask UI contributors. +class NpmViteWebDevServer implements LaunchWebDevServer { + NpmViteWebDevServer({ + Directory? webAppRoot, + ViteDevServerProcessStarter startProcess = Process.start, + }) : _webAppRoot = webAppRoot, + _startProcess = startProcess; + + final Directory? _webAppRoot; + final ViteDevServerProcessStarter _startProcess; + + @override + Future start({required String projectRoot}) async { + final Directory webAppRoot = _webAppRoot ?? await _resolveWebAppRoot(); + late final Process process; + try { + process = await _startProcess( + 'npm', + const ['run', 'dev', '--', '--host', '127.0.0.1'], + workingDirectory: webAppRoot.path, + ); + } catch (_) { + throw const LaunchWebDevException('web_dev_start_failed'); + } + + final Completer urlCompleter = Completer(); + final StringBuffer startupOutput = StringBuffer(); + + void observeOutput(List data) { + final String text = utf8.decode(data, allowMalformed: true); + startupOutput.write(text); + final Uri? devServerUrl = + parseViteDevServerUrlFromOutput(startupOutput.toString()); + if (devServerUrl != null && !urlCompleter.isCompleted) { + urlCompleter.complete(devServerUrl); + } + } + + process.stdout.listen(observeOutput); + process.stderr.listen(observeOutput); + process.exitCode.then((int exitCode) { + if (!urlCompleter.isCompleted) { + urlCompleter.completeError( + const LaunchWebDevException('web_dev_start_failed'), + ); + } + }); + + try { + return await urlCompleter.future.timeout(const Duration(seconds: 30)); + } on LaunchWebDevException { + rethrow; + } on TimeoutException { + process.kill(); + throw const LaunchWebDevException('web_dev_url_not_found'); + } catch (_) { + throw const LaunchWebDevException('web_dev_start_failed'); + } + } +} + +Future _resolveWebAppRoot() async { + final Uri? launchLibraryUri = await Isolate.resolvePackageUri( + Uri.parse('package:ask_ui_bridge/launch/launch_command.dart'), + ); + if (launchLibraryUri == null) { + return Directory('../web'); + } + + return Directory.fromUri(launchLibraryUri.resolve('../../../web')); +} diff --git a/apps/bridge/lib/launch/launch_workbench_url.dart b/apps/bridge/lib/launch/launch_workbench_url.dart new file mode 100644 index 0000000..6d29724 --- /dev/null +++ b/apps/bridge/lib/launch/launch_workbench_url.dart @@ -0,0 +1,38 @@ +part of 'launch_command.dart'; + +/// Build the workbench URL that attaches to an existing Bridge Session. +/// +/// Packaged mode uses the Bridge Server as the workbench base URL. Web dev +/// mode uses the Vite URL while still pointing `bridgeUrl` at the Bridge Server. +Uri buildLaunchWorkbenchUrl({ + Uri? workbenchBaseUrl, + required Uri bridgeUrl, + required String sessionId, + required String selectedDeviceId, + required String projectRoot, + required String? flavor, + required String? target, +}) { + final Map queryParameters = { + 'bridgeUrl': _withoutTrailingSlash(bridgeUrl.toString()), + 'sessionId': sessionId, + 'deviceId': selectedDeviceId, + 'projectRoot': projectRoot, + }; + if (flavor != null) { + queryParameters['flavor'] = flavor; + } + if (target != null) { + queryParameters['target'] = target; + } + + final Uri baseUrl = workbenchBaseUrl ?? bridgeUrl; + return baseUrl.replace( + path: '/', + queryParameters: queryParameters, + ); +} + +String _withoutTrailingSlash(String value) { + return value.replaceFirst(RegExp(r'/+$'), ''); +} diff --git a/apps/bridge/pubspec.yaml b/apps/bridge/pubspec.yaml index f072d1e..7e3c034 100644 --- a/apps/bridge/pubspec.yaml +++ b/apps/bridge/pubspec.yaml @@ -1,12 +1,17 @@ name: ask_ui_bridge -description: Local bridge server for Ask UI. -publish_to: none +description: Local bridge server and launcher for Ask UI. +version: 0.0.4 +repository: https://github.com/drown0315/ask_ui +homepage: https://github.com/drown0315/ask_ui + +executables: + ask_ui_bridge: ask_ui_bridge environment: sdk: '>=3.4.0 <4.0.0' dependencies: - vm_service: 14.3.1 + vm_service: ^14.3.1 dev_dependencies: file_testkit: 1.0.0 diff --git a/apps/bridge/test/device/scrcpy_device_stream_test.dart b/apps/bridge/test/device/scrcpy_device_stream_test.dart index bceb878..d917c94 100644 --- a/apps/bridge/test/device/scrcpy_device_stream_test.dart +++ b/apps/bridge/test/device/scrcpy_device_stream_test.dart @@ -107,6 +107,34 @@ void main() { expect(config.serverPath, isNot('/tmp/ignored-scrcpy-server')); }); + test('resolves the vendored scrcpy server relative to the bridge package', + () async { + final externalProject = await Directory.systemTemp.createTemp( + 'ask_ui_external_flutter_project_', + ); + addTearDown(() async { + if (externalProject.existsSync()) { + await externalProject.delete(recursive: true); + } + }); + + final bridgeRoot = Directory.current.absolute; + final bridgeEntrypoint = bridgeRoot.uri.resolve('bin/ask_ui_bridge.dart'); + final serverPath = defaultScrcpyServerPath( + currentDirectory: externalProject, + scriptUri: bridgeEntrypoint, + ); + + expect(serverPath, isNot(contains(externalProject.path))); + expect( + serverPath, + bridgeRoot.uri + .resolve('vendor/scrcpy/4.0/scrcpy-server-v4.0') + .toFilePath(), + ); + expect(File(serverPath).existsSync(), isTrue); + }); + test('generates a fresh scrcpy socket id for each factory start', () async { final runner = FakeScrcpyCommandRunner(); final sink = RecordingDeviceStreamSink(); diff --git a/apps/bridge/test/launch/launch_command_test.dart b/apps/bridge/test/launch/launch_command_test.dart index 60ba54a..60ddd23 100644 --- a/apps/bridge/test/launch/launch_command_test.dart +++ b/apps/bridge/test/launch/launch_command_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -59,6 +60,7 @@ void main() { 'dartDefines': ['API_HOST=local', 'FEATURE_X=true'], 'projectRoot': '/workspace/app', 'open': false, + 'webDev': false, }, 'bridgeUrl': 'http://127.0.0.1:8787', 'sessionId': 'session-1', @@ -67,7 +69,7 @@ void main() { 'flavor': 'staging', 'target': 'lib/main_staging.dart', 'agentCommand': - 'dart run ask_ui_bridge agent poll --base-url http://127.0.0.1:8787 --session-id session-1', + 'ask_ui_bridge agent poll --base-url http://127.0.0.1:8787 --session-id session-1', 'workbenchUrl': 'http://127.0.0.1:8787/?bridgeUrl=http%3A%2F%2F127.0.0.1%3A8787&sessionId=session-1&deviceId=device-1&projectRoot=%2Fworkspace%2Fapp&flavor=staging&target=lib%2Fmain_staging.dart', 'browserOpened': false, @@ -133,6 +135,7 @@ void main() { 'dartDefines': ['API_HOST=local'], 'projectRoot': '/workspace/app', 'open': true, + 'webDev': false, }, 'bridgeUrl': 'http://127.0.0.1:9876', 'sessionId': 'session-7', @@ -141,7 +144,7 @@ void main() { 'flavor': 'dev', 'target': 'lib/main_dev.dart', 'agentCommand': - 'dart run ask_ui_bridge agent poll --base-url http://127.0.0.1:9876 --session-id session-7', + 'ask_ui_bridge agent poll --base-url http://127.0.0.1:9876 --session-id session-7', 'workbenchUrl': 'http://127.0.0.1:9876/?bridgeUrl=http%3A%2F%2F127.0.0.1%3A9876&sessionId=session-7&deviceId=device-1&projectRoot=%2Fworkspace%2Fapp&flavor=dev&target=lib%2Fmain_dev.dart', 'browserOpened': true, @@ -156,6 +159,7 @@ void main() { expect(appLauncher.requests.single.projectRoot, '/workspace/app'); expect(appLauncher.requests.single.arguments, [ 'run', + '--machine', '--device-id', 'device-1', '--flavor', @@ -170,6 +174,83 @@ void main() { vmServiceUri: 'ws://127.0.0.1:44444/ws', projectRoot: '/workspace/app', deviceId: 'device-1', + requirePackagedWeb: true, + ), + ]); + }); + + test('web dev mode opens Vite workbench with session attach parameters', + () async { + final _FakeFlutterDevices devices = _FakeFlutterDevices([ + { + 'id': 'device-1', + 'name': 'Pixel 6', + 'targetPlatform': 'android-arm64', + }, + ]); + final _RecordingBrowserOpener browserOpener = _RecordingBrowserOpener(); + final _RecordingWebDevServer webDevServer = _RecordingWebDevServer( + devServerUrl: Uri.parse('http://127.0.0.1:5174'), + ); + final _RecordingBridgeLauncher bridgeLauncher = _RecordingBridgeLauncher( + bridgeUrl: Uri.parse('http://127.0.0.1:9876'), + sessionId: 'session-7', + ); + + final LaunchCommandResult result = await runLaunchCommand( + const [ + 'launch', + '--device', + 'device-1', + '--project-root', + '/workspace/app', + '--web-dev', + ], + listDevices: devices.listDevices, + appLauncher: _RecordingAppLauncher( + vmServiceUri: 'ws://127.0.0.1:44444/ws', + ), + bridgeLauncher: bridgeLauncher, + browserOpener: browserOpener, + webDevServer: webDevServer, + ); + + expect(result.exitCode, 0); + expect(result.stderr, isEmpty); + expect(jsonDecode(result.stdout), { + 'status': 'ready', + 'selectedDevice': { + 'id': 'device-1', + 'name': 'Pixel 6', + 'targetPlatform': 'android-arm64', + }, + 'launchIntent': { + 'device': 'device-1', + 'flavor': null, + 'target': null, + 'dartDefines': [], + 'projectRoot': '/workspace/app', + 'open': true, + 'webDev': true, + }, + 'bridgeUrl': 'http://127.0.0.1:9876', + 'sessionId': 'session-7', + 'vmServiceUri': 'ws://127.0.0.1:44444/ws', + 'projectRoot': '/workspace/app', + 'flavor': null, + 'target': null, + 'agentCommand': + 'ask_ui_bridge agent poll --base-url http://127.0.0.1:9876 --session-id session-7', + 'workbenchUrl': + 'http://127.0.0.1:5174/?bridgeUrl=http%3A%2F%2F127.0.0.1%3A9876&sessionId=session-7&deviceId=device-1&projectRoot=%2Fworkspace%2Fapp', + 'browserOpened': true, + 'nextStep': 'Run the returned agent poll command.', + }); + expect(webDevServer.startRequests, ['/workspace/app']); + expect(bridgeLauncher.requests.single.requirePackagedWeb, false); + expect(browserOpener.openedUrls, [ + Uri.parse( + 'http://127.0.0.1:5174/?bridgeUrl=http%3A%2F%2F127.0.0.1%3A9876&sessionId=session-7&deviceId=device-1&projectRoot=%2Fworkspace%2Fapp', ), ]); }); @@ -289,6 +370,77 @@ void main() { }); }); + test('reports missing packaged Web when default launch has no workbench', + () async { + final Directory packagedWebRoot = + await Directory.systemTemp.createTemp('ask-ui-empty-web-'); + addTearDown(() async { + if (await packagedWebRoot.exists()) { + await packagedWebRoot.delete(recursive: true); + } + }); + final _FakeFlutterDevices devices = _FakeFlutterDevices([ + { + 'id': 'device-1', + 'name': 'Pixel 6', + 'targetPlatform': 'android-arm64', + }, + ]); + + final LaunchCommandResult result = await runLaunchCommand( + const ['launch', '--device', 'device-1'], + listDevices: devices.listDevices, + appLauncher: _RecordingAppLauncher( + vmServiceUri: 'ws://127.0.0.1:44444/ws', + ), + bridgeLauncher: LocalBridgeLauncher(packagedWebRoot: packagedWebRoot), + browserOpener: _RecordingBrowserOpener(), + ); + + expect(result.exitCode, 1); + expect(result.stdout, isEmpty); + expect(jsonDecode(result.stderr), { + 'status': 'error', + 'error': 'packaged_web_not_found', + }); + }); + + test('reports Web dev startup failures before opening the browser', + () async { + final _FakeFlutterDevices devices = _FakeFlutterDevices([ + { + 'id': 'device-1', + 'name': 'Pixel 6', + 'targetPlatform': 'android-arm64', + }, + ]); + final _RecordingBrowserOpener browserOpener = _RecordingBrowserOpener(); + + final LaunchCommandResult result = await runLaunchCommand( + const ['launch', '--device', 'device-1', '--web-dev'], + listDevices: devices.listDevices, + appLauncher: _RecordingAppLauncher( + vmServiceUri: 'ws://127.0.0.1:44444/ws', + ), + bridgeLauncher: _RecordingBridgeLauncher( + bridgeUrl: Uri.parse('http://127.0.0.1:9876'), + sessionId: 'session-7', + ), + browserOpener: browserOpener, + webDevServer: const _FailingWebDevServer( + LaunchWebDevException('web_dev_start_failed'), + ), + ); + + expect(result.exitCode, 1); + expect(result.stdout, isEmpty); + expect(jsonDecode(result.stderr), { + 'status': 'error', + 'error': 'web_dev_start_failed', + }); + expect(browserOpener.openedUrls, isEmpty); + }); + test('parses Flutter VM Service output into WebSocket URIs', () { expect( parseFlutterVmServiceUriFromOutput( @@ -304,12 +456,123 @@ void main() { ), 'ws://127.0.0.1:51234/abc_def=/ws', ); + expect( + parseFlutterVmServiceUriFromOutput( + 'See https://flutter.dev/to/flutter-gradle-plugin-apply ' + 'for migration details.', + ), + isNull, + ); + expect( + parseFlutterVmServiceUriFromOutput( + 'See https://flutter.dev/to/flutter-gradle-plugin-apply ' + 'for migration details.\n' + 'A Dart VM Service is available at: ' + 'http://127.0.0.1:51234/abc_def=/', + ), + 'ws://127.0.0.1:51234/abc_def=/ws', + ); expect( parseFlutterVmServiceUriFromOutput('Build failed before service.'), isNull, ); }); + test('parses Flutter machine output into WebSocket URIs', () { + expect( + parseFlutterVmServiceUriFromMachineOutput( + jsonEncode([ + { + 'event': 'app.debugPort', + 'params': { + 'wsUri': 'ws://127.0.0.1:51234/abc_def=/ws', + }, + }, + ]), + ), + 'ws://127.0.0.1:51234/abc_def=/ws', + ); + expect( + parseFlutterVmServiceUriFromMachineOutput( + jsonEncode([ + { + 'id': 0, + 'result': { + 'started': true, + 'vmServiceUri': 'http://127.0.0.1:51234/abc_def=/', + }, + }, + ]), + ), + 'ws://127.0.0.1:51234/abc_def=/ws', + ); + expect( + parseFlutterVmServiceUriFromMachineOutput( + jsonEncode([ + { + 'event': 'daemon.logMessage', + 'params': { + 'message': + 'See https://flutter.dev/to/flutter-gradle-plugin-apply', + }, + }, + ]), + ), + isNull, + ); + }); + + test('parses Vite dev server output into localhost URLs', () { + expect( + parseViteDevServerUrlFromOutput(' Local: http://127.0.0.1:5174/'), + Uri.parse('http://127.0.0.1:5174/'), + ); + expect( + parseViteDevServerUrlFromOutput( + 'Port 5173 is in use, trying another one...\n' + ' Local: http://localhost:5175/', + ), + Uri.parse('http://localhost:5175/'), + ); + expect( + parseViteDevServerUrlFromOutput('ready in 312 ms'), + isNull, + ); + }); + + test('starts Vite from the Web app package and returns its printed URL', + () async { + final _RecordingProcessStarter processStarter = _RecordingProcessStarter( + stdoutChunks: [ + utf8.encode('Port 5173 is in use, trying another one...\n'), + utf8.encode(' Local: http://127.0.0.1:5174/\n'), + ], + ); + final NpmViteWebDevServer webDevServer = NpmViteWebDevServer( + webAppRoot: Directory('/workspace/ask_ui/apps/web'), + startProcess: processStarter.start, + ); + + final Uri devServerUrl = await webDevServer.start( + projectRoot: '/workspace/flutter_app', + ); + + expect(devServerUrl, Uri.parse('http://127.0.0.1:5174/')); + expect(processStarter.requests, hasLength(1)); + expect(processStarter.requests.single.executable, 'npm'); + expect(processStarter.requests.single.arguments, [ + 'run', + 'dev', + '--', + '--host', + '127.0.0.1', + ]); + expect( + processStarter.requests.single.workingDirectory, + '/workspace/ask_ui/apps/web', + ); + }); + test('returns one JSON error object when no usable devices exist', () async { final _FakeFlutterDevices devices = _FakeFlutterDevices([ @@ -384,14 +647,14 @@ void main() { 'name': 'Pixel API 35', 'targetPlatform': 'android-x64', 'suggestedCommand': - 'dart run ask_ui_bridge launch --device emulator-5554 --flavor dev --target lib/main_dev.dart --dart-define A=B --project-root /workspace/app --no-open', + 'ask_ui_bridge launch --device emulator-5554 --flavor dev --target lib/main_dev.dart --dart-define A=B --project-root /workspace/app --no-open', }, { 'id': '19271FDF6007TY', 'name': 'Pixel 6', 'targetPlatform': 'android-arm64', 'suggestedCommand': - 'dart run ask_ui_bridge launch --device 19271FDF6007TY --flavor dev --target lib/main_dev.dart --dart-define A=B --project-root /workspace/app --no-open', + 'ask_ui_bridge launch --device 19271FDF6007TY --flavor dev --target lib/main_dev.dart --dart-define A=B --project-root /workspace/app --no-open', }, ], 'launchIntent': { @@ -401,6 +664,7 @@ void main() { 'dartDefines': ['A=B'], 'projectRoot': '/workspace/app', 'open': false, + 'webDev': false, }, 'nextStep': 'Ask the user to choose a device, then rerun launch with --device.', @@ -583,20 +847,31 @@ class _RecordingBridgeLauncher implements LaunchBridgeLauncher { final Uri bridgeUrl; final String sessionId; - final List<({String vmServiceUri, String projectRoot, String deviceId})> - requests = - <({String vmServiceUri, String projectRoot, String deviceId})>[]; + final List< + ({ + String vmServiceUri, + String projectRoot, + String deviceId, + bool requirePackagedWeb, + })> requests = <({ + String vmServiceUri, + String projectRoot, + String deviceId, + bool requirePackagedWeb, + })>[]; @override Future createSession({ required String vmServiceUri, required String projectRoot, required String deviceId, + required bool requirePackagedWeb, }) async { requests.add(( vmServiceUri: vmServiceUri, projectRoot: projectRoot, deviceId: deviceId, + requirePackagedWeb: requirePackagedWeb, )); return LaunchBridgeSession( bridgeUrl: bridgeUrl, @@ -618,6 +893,7 @@ class _FailingBridgeLauncher implements LaunchBridgeLauncher { required String vmServiceUri, required String projectRoot, required String deviceId, + required bool requirePackagedWeb, }) async { throw exception; } @@ -641,6 +917,122 @@ class _FailingBrowserOpener implements LaunchBrowserOpener { } } +class _RecordingWebDevServer implements LaunchWebDevServer { + _RecordingWebDevServer({required this.devServerUrl}); + + final Uri devServerUrl; + final List startRequests = []; + + @override + Future start({required String projectRoot}) async { + startRequests.add(projectRoot); + return devServerUrl; + } +} + +class _FailingWebDevServer implements LaunchWebDevServer { + const _FailingWebDevServer(this.exception); + + final LaunchWebDevException exception; + + @override + Future start({required String projectRoot}) async { + throw exception; + } +} + +class _RecordingProcessStarter { + _RecordingProcessStarter({required this.stdoutChunks}); + + final List> stdoutChunks; + final List< + ({ + String executable, + List arguments, + String? workingDirectory, + })> requests = <({ + String executable, + List arguments, + String? workingDirectory, + })>[]; + + Future start( + String executable, + List arguments, { + String? workingDirectory, + }) async { + requests.add(( + executable: executable, + arguments: List.from(arguments), + workingDirectory: workingDirectory, + )); + return _FakeProcess(stdoutChunks: stdoutChunks); + } +} + +class _FakeProcess implements Process { + _FakeProcess({required List> stdoutChunks}) + : stdout = Stream>.fromIterable(stdoutChunks), + stderr = const Stream>.empty(), + stdin = _FakeIOSink(), + exitCode = Completer().future; + + @override + final Stream> stdout; + + @override + final Stream> stderr; + + @override + final IOSink stdin; + + @override + final Future exitCode; + + @override + int get pid => 1; + + @override + bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { + return true; + } +} + +class _FakeIOSink implements IOSink { + @override + Encoding encoding = utf8; + + @override + void add(List data) {} + + @override + void addError(Object error, [StackTrace? stackTrace]) {} + + @override + Future addStream(Stream> stream) async {} + + @override + Future close() async {} + + @override + Future get done => Future.value(); + + @override + Future flush() async {} + + @override + void write(Object? object) {} + + @override + void writeAll(Iterable objects, [String separator = '']) {} + + @override + void writeCharCode(int charCode) {} + + @override + void writeln([Object? object = '']) {} +} + class _FakeFlutterDevices { _FakeFlutterDevices(this.devices); diff --git a/apps/bridge/test/launch/launch_skill_workflow_test.dart b/apps/bridge/test/launch/launch_skill_workflow_test.dart new file mode 100644 index 0000000..1f7f0fc --- /dev/null +++ b/apps/bridge/test/launch/launch_skill_workflow_test.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + group('Ask UI launch skill workflow', () { + test('documents the installed user launch and poll workflow', () { + final File skillFile = File('../../skills/ask-ui/SKILL.md'); + + expect(skillFile.existsSync(), isTrue); + final String skill = skillFile.readAsStringSync(); + + expect(skill, contains('ask_ui')); + expect(skill, contains('dart pub global activate ask_ui_bridge')); + expect(skill, contains('ask_ui_bridge launch')); + expect(skill, contains('ask_ui_runtime')); + expect(skill, contains('flutter pub add ask_ui_runtime')); + expect(skill, contains('registerAskUiRuntime')); + expect(skill, isNot(contains('dart run bin/ask_ui_bridge.dart launch'))); + expect(skill, isNot(contains('--web-dev'))); + _expectCommonAgentLoopContract(skill); + }); + + test('documents the maintainer source launch and Web dev workflow', () { + final File skillFile = File('../../skills/ask-ui-dev/SKILL.md'); + + expect(skillFile.existsSync(), isTrue); + final String skill = skillFile.readAsStringSync(); + + expect(skill, contains('ask-ui-dev')); + expect(skill, contains('dart run bin/ask_ui_bridge.dart launch')); + expect(skill, contains('--web-dev')); + expect(skill, contains('Vite')); + _expectCommonAgentLoopContract(skill); + }); + }); +} + +void _expectCommonAgentLoopContract(String skill) { + expect(skill, contains('--device ')); + expect(skill, contains('--flavor ')); + expect(skill, contains('--target ')); + expect(skill, contains('--dart-define ')); + expect(skill, contains('needs_device_selection')); + expect(skill, contains('suggestedCommand')); + expect(skill, contains('ready')); + expect(skill, contains('agentCommand')); + expect(skill, contains('current-session task input')); + expect( + skill, + contains('agent poll --reply-to --agent-reply '), + ); + expect(skill, contains('--agent-error')); + expect(skill, contains('continue polling')); + expect(skill, contains('Do not duplicate')); + expect(skill, contains('Flutter, bridge, Web, or browser')); +} diff --git a/apps/bridge/test/release/release_layout_validator_test.dart b/apps/bridge/test/release/release_layout_validator_test.dart new file mode 100644 index 0000000..19f6dbf --- /dev/null +++ b/apps/bridge/test/release/release_layout_validator_test.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:file_testkit/file_testkit.dart'; +import 'package:test/test.dart'; + +import '../../tool/release_layout_validator.dart'; + +void main() { + group('ReleaseLayoutValidator', () { + test('accepts packaged Web files required by installed launch', () async { + await FileTestkit.runZoned(() async { + final Directory packageRoot = Directory('/ask-ui-bridge-package'); + await Directory('${packageRoot.path}/web/assets').create( + recursive: true, + ); + await File('${packageRoot.path}/web/index.html').writeAsString( + '
', + ); + await File('${packageRoot.path}/web/assets/index.js').writeAsString( + 'console.log("ask ui");', + ); + await File('${packageRoot.path}/web/assets/index.css').writeAsString( + 'body { margin: 0; }', + ); + + final ReleaseLayoutResult result = ReleaseLayoutValidator.validate( + packageRoot: packageRoot, + ); + + expect(result.isValid, isTrue); + expect(result.errors, isEmpty); + }); + }); + + test('reports every missing packaged Web requirement', () async { + await FileTestkit.runZoned(() async { + final Directory packageRoot = Directory('/ask-ui-bridge-package'); + await Directory('${packageRoot.path}/web/assets').create( + recursive: true, + ); + + final ReleaseLayoutResult result = ReleaseLayoutValidator.validate( + packageRoot: packageRoot, + ); + + expect(result.isValid, isFalse); + expect( + result.errors, + containsAll([ + 'Missing web/index.html.', + 'Missing JavaScript asset under web/assets.', + 'Missing CSS asset under web/assets.', + ]), + ); + }); + }); + }); +} diff --git a/apps/bridge/tool/release_layout_validator.dart b/apps/bridge/tool/release_layout_validator.dart new file mode 100644 index 0000000..f50b3b6 --- /dev/null +++ b/apps/bridge/tool/release_layout_validator.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +/// Validation result for the files required by the published bridge package. +/// +/// A valid bridge package contains the Web workbench files copied from the +/// Vite build into `web/`. Installed users rely on those files because the +/// bridge serves the workbench from the pub package at launch time. +class ReleaseLayoutResult { + const ReleaseLayoutResult(this.errors); + + final List errors; + + bool get isValid => errors.isEmpty; +} + +/// Checks that the bridge package contains the packaged Web workbench. +class ReleaseLayoutValidator { + ReleaseLayoutValidator._(); + + /// Validate required packaged Web files under [packageRoot]. + /// + /// The validator expects: + /// - `web/index.html` + /// - at least one JavaScript asset under `web/assets` + /// - at least one CSS asset under `web/assets` + /// + /// Missing files are reported together so release scripts can print one + /// complete failure message instead of failing one file at a time. + static ReleaseLayoutResult validate({required Directory packageRoot}) { + final List errors = []; + final File indexFile = File('${packageRoot.path}/web/index.html'); + final Directory assetsDirectory = Directory( + '${packageRoot.path}/web/assets', + ); + + if (!indexFile.existsSync()) { + errors.add('Missing web/index.html.'); + } + + final List assetFiles = assetsDirectory.existsSync() + ? assetsDirectory.listSync(recursive: true).whereType().toList() + : []; + + if (!assetFiles.any((File file) => file.path.endsWith('.js'))) { + errors.add('Missing JavaScript asset under web/assets.'); + } + + if (!assetFiles.any((File file) => file.path.endsWith('.css'))) { + errors.add('Missing CSS asset under web/assets.'); + } + + return ReleaseLayoutResult(errors); + } +} diff --git a/apps/bridge/tool/validate_release_layout.dart b/apps/bridge/tool/validate_release_layout.dart new file mode 100644 index 0000000..46a4ee0 --- /dev/null +++ b/apps/bridge/tool/validate_release_layout.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'release_layout_validator.dart'; + +/// CLI entrypoint for checking the bridge package layout before pub publish. +void main(List args) { + final String packageRootPath = + args.isEmpty ? Directory.current.path : args[0]; + final ReleaseLayoutResult result = ReleaseLayoutValidator.validate( + packageRoot: Directory(packageRootPath), + ); + + if (result.isValid) { + stdout.writeln('Bridge release layout is valid.'); + return; + } + + stderr.writeln('Bridge release layout is invalid:'); + for (final String error in result.errors) { + stderr.writeln('- $error'); + } + exitCode = 1; +} diff --git a/apps/bridge/web/README.md b/apps/bridge/web/README.md deleted file mode 100644 index 4fdb8b4..0000000 --- a/apps/bridge/web/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Packaged Ask UI Web - -Release preparation copies the built Ask UI Web workbench into this directory -so the Bridge Server can serve it from the Dart package. diff --git a/apps/bridge/web/assets/index-CKHto2yr.js b/apps/bridge/web/assets/index-CKHto2yr.js new file mode 100644 index 0000000..0d56659 --- /dev/null +++ b/apps/bridge/web/assets/index-CKHto2yr.js @@ -0,0 +1,9 @@ +(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))f(d);new MutationObserver(d=>{for(const p of d)if(p.type==="childList")for(const T of p.addedNodes)T.tagName==="LINK"&&T.rel==="modulepreload"&&f(T)}).observe(document,{childList:!0,subtree:!0});function r(d){const p={};return d.integrity&&(p.integrity=d.integrity),d.referrerPolicy&&(p.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?p.credentials="include":d.crossOrigin==="anonymous"?p.credentials="omit":p.credentials="same-origin",p}function f(d){if(d.ep)return;d.ep=!0;const p=r(d);fetch(d.href,p)}})();var ds={exports:{}},zn={};var Cd;function Yg(){if(Cd)return zn;Cd=1;var i=Symbol.for("react.transitional.element"),s=Symbol.for("react.fragment");function r(f,d,p){var T=null;if(p!==void 0&&(T=""+p),d.key!==void 0&&(T=""+d.key),"key"in d){p={};for(var _ in d)_!=="key"&&(p[_]=d[_])}else p=d;return d=p.ref,{$$typeof:i,type:f,key:T,ref:d!==void 0?d:null,props:p}}return zn.Fragment=s,zn.jsx=r,zn.jsxs=r,zn}var xd;function Gg(){return xd||(xd=1,ds.exports=Yg()),ds.exports}var S=Gg(),hs={exports:{}},k={};var Od;function Xg(){if(Od)return k;Od=1;var i=Symbol.for("react.transitional.element"),s=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),d=Symbol.for("react.profiler"),p=Symbol.for("react.consumer"),T=Symbol.for("react.context"),_=Symbol.for("react.forward_ref"),C=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),D=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),H=Symbol.iterator;function O(y){return y===null||typeof y!="object"?null:(y=H&&y[H]||y["@@iterator"],typeof y=="function"?y:null)}var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},L=Object.assign,W={};function w(y,j,G){this.props=y,this.context=j,this.refs=W,this.updater=G||B}w.prototype.isReactComponent={},w.prototype.setState=function(y,j){if(typeof y!="object"&&typeof y!="function"&&y!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,y,j,"setState")},w.prototype.forceUpdate=function(y){this.updater.enqueueForceUpdate(this,y,"forceUpdate")};function et(){}et.prototype=w.prototype;function lt(y,j,G){this.props=y,this.context=j,this.refs=W,this.updater=G||B}var bt=lt.prototype=new et;bt.constructor=lt,L(bt,w.prototype),bt.isPureReactComponent=!0;var F=Array.isArray;function ot(){}var V={H:null,A:null,T:null,S:null},Et=Object.prototype.hasOwnProperty;function Nt(y,j,G){var Q=G.ref;return{$$typeof:i,type:y,key:j,ref:Q!==void 0?Q:null,props:G}}function Mt(y,j){return Nt(y.type,j,y.props)}function jt(y){return typeof y=="object"&&y!==null&&y.$$typeof===i}function Yt(y){var j={"=":"=0",":":"=2"};return"$"+y.replace(/[=:]/g,function(G){return j[G]})}var zl=/\/+/g;function De(y,j){return typeof y=="object"&&y!==null&&y.key!=null?Yt(""+y.key):j.toString(36)}function _e(y){switch(y.status){case"fulfilled":return y.value;case"rejected":throw y.reason;default:switch(typeof y.status=="string"?y.then(ot,ot):(y.status="pending",y.then(function(j){y.status==="pending"&&(y.status="fulfilled",y.value=j)},function(j){y.status==="pending"&&(y.status="rejected",y.reason=j)})),y.status){case"fulfilled":return y.value;case"rejected":throw y.reason}}throw y}function M(y,j,G,Q,I){var at=typeof y;(at==="undefined"||at==="boolean")&&(y=null);var ht=!1;if(y===null)ht=!0;else switch(at){case"bigint":case"string":case"number":ht=!0;break;case"object":switch(y.$$typeof){case i:case s:ht=!0;break;case D:return ht=y._init,M(ht(y._payload),j,G,Q,I)}}if(ht)return I=I(y),ht=Q===""?"."+De(y,0):Q,F(I)?(G="",ht!=null&&(G=ht.replace(zl,"$&/")+"/"),M(I,j,G,"",function(Ma){return Ma})):I!=null&&(jt(I)&&(I=Mt(I,G+(I.key==null||y&&y.key===I.key?"":(""+I.key).replace(zl,"$&/")+"/")+ht)),j.push(I)),1;ht=0;var $t=Q===""?".":Q+":";if(F(y))for(var Rt=0;Rt>>1,Tt=M[yt];if(0>>1;ytd(G,$))Qd(I,G)?(M[yt]=I,M[Q]=$,yt=Q):(M[yt]=G,M[j]=$,yt=j);else if(Qd(I,$))M[yt]=I,M[Q]=$,yt=Q;else break t}}return Y}function d(M,Y){var $=M.sortIndex-Y.sortIndex;return $!==0?$:M.id-Y.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var p=performance;i.unstable_now=function(){return p.now()}}else{var T=Date,_=T.now();i.unstable_now=function(){return T.now()-_}}var C=[],g=[],D=1,b=null,H=3,O=!1,B=!1,L=!1,W=!1,w=typeof setTimeout=="function"?setTimeout:null,et=typeof clearTimeout=="function"?clearTimeout:null,lt=typeof setImmediate<"u"?setImmediate:null;function bt(M){for(var Y=r(g);Y!==null;){if(Y.callback===null)f(g);else if(Y.startTime<=M)f(g),Y.sortIndex=Y.expirationTime,s(C,Y);else break;Y=r(g)}}function F(M){if(L=!1,bt(M),!B)if(r(C)!==null)B=!0,ot||(ot=!0,Yt());else{var Y=r(g);Y!==null&&_e(F,Y.startTime-M)}}var ot=!1,V=-1,Et=5,Nt=-1;function Mt(){return W?!0:!(i.unstable_now()-NtM&&Mt());){var yt=b.callback;if(typeof yt=="function"){b.callback=null,H=b.priorityLevel;var Tt=yt(b.expirationTime<=M);if(M=i.unstable_now(),typeof Tt=="function"){b.callback=Tt,bt(M),Y=!0;break e}b===r(C)&&f(C),bt(M)}else f(C);b=r(C)}if(b!==null)Y=!0;else{var y=r(g);y!==null&&_e(F,y.startTime-M),Y=!1}}break t}finally{b=null,H=$,O=!1}Y=void 0}}finally{Y?Yt():ot=!1}}}var Yt;if(typeof lt=="function")Yt=function(){lt(jt)};else if(typeof MessageChannel<"u"){var zl=new MessageChannel,De=zl.port2;zl.port1.onmessage=jt,Yt=function(){De.postMessage(null)}}else Yt=function(){w(jt,0)};function _e(M,Y){V=w(function(){M(i.unstable_now())},Y)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(M){M.callback=null},i.unstable_forceFrameRate=function(M){0>M||125yt?(M.sortIndex=$,s(g,M),r(C)===null&&M===r(g)&&(L?(et(V),V=-1):L=!0,_e(F,$-yt))):(M.sortIndex=Tt,s(C,M),B||O||(B=!0,ot||(ot=!0,Yt()))),M},i.unstable_shouldYield=Mt,i.unstable_wrapCallback=function(M){var Y=H;return function(){var $=H;H=Y;try{return M.apply(this,arguments)}finally{H=$}}}})(ys)),ys}var Ud;function Vg(){return Ud||(Ud=1,gs.exports=Qg()),gs.exports}var vs={exports:{}},Wt={};var Dd;function Zg(){if(Dd)return Wt;Dd=1;var i=Ts();function s(C){var g="https://react.dev/errors/"+C;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(s){console.error(s)}}return i(),vs.exports=Zg(),vs.exports}var Hd;function Jg(){if(Hd)return _n;Hd=1;var i=Vg(),s=Ts(),r=Kg();function f(t){var e="https://react.dev/errors/"+t;if(1Tt||(t.current=yt[Tt],yt[Tt]=null,Tt--)}function G(t,e){Tt++,yt[Tt]=t.current,t.current=e}var Q=y(null),I=y(null),at=y(null),ht=y(null);function $t(t,e){switch(G(at,e),G(I,t),G(Q,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?ko(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=ko(e),t=Fo(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}j(Q),G(Q,t)}function Rt(){j(Q),j(I),j(at)}function Ma(t){t.memoizedState!==null&&G(ht,t);var e=Q.current,l=Fo(e,t.type);e!==l&&(G(I,t),G(Q,l))}function xn(t){I.current===t&&(j(Q),j(I)),ht.current===t&&(j(ht),bn._currentValue=$)}var Wu,zs;function _l(t){if(Wu===void 0)try{throw Error()}catch(l){var e=l.stack.trim().match(/\n( *(at )?)/);Wu=e&&e[1]||"",zs=-1)":-1n||h[a]!==E[n]){var x=` +`+h[a].replace(" at new "," at ");return t.displayName&&x.includes("")&&(x=x.replace("",t.displayName)),x}while(1<=a&&0<=n);break}}}finally{$u=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?_l(l):""}function vh(t,e){switch(t.tag){case 26:case 27:case 5:return _l(t.type);case 16:return _l("Lazy");case 13:return t.child!==e&&e!==null?_l("Suspense Fallback"):_l("Suspense");case 19:return _l("SuspenseList");case 0:case 15:return ku(t.type,!1);case 11:return ku(t.type.render,!1);case 1:return ku(t.type,!0);case 31:return _l("Activity");default:return""}}function _s(t){try{var e="",l=null;do e+=vh(t,l),l=t,t=t.return;while(t);return e}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var Fu=Object.prototype.hasOwnProperty,Iu=i.unstable_scheduleCallback,Pu=i.unstable_cancelCallback,Sh=i.unstable_shouldYield,ph=i.unstable_requestPaint,ne=i.unstable_now,bh=i.unstable_getCurrentPriorityLevel,Ns=i.unstable_ImmediatePriority,Cs=i.unstable_UserBlockingPriority,On=i.unstable_NormalPriority,Th=i.unstable_LowPriority,xs=i.unstable_IdlePriority,Ah=i.log,Eh=i.unstable_setDisableYieldValue,Ra=null,ue=null;function Ie(t){if(typeof Ah=="function"&&Eh(t),ue&&typeof ue.setStrictMode=="function")try{ue.setStrictMode(Ra,t)}catch{}}var ie=Math.clz32?Math.clz32:Nh,zh=Math.log,_h=Math.LN2;function Nh(t){return t>>>=0,t===0?32:31-(zh(t)/_h|0)|0}var Mn=256,Rn=262144,Un=4194304;function Nl(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Dn(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,c=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=Nl(a):(c&=o,c!==0?n=Nl(c):l||(l=o&~t,l!==0&&(n=Nl(l))))):(o=a&~u,o!==0?n=Nl(o):c!==0?n=Nl(c):l||(l=a&~t,l!==0&&(n=Nl(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function Ua(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Ch(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Os(){var t=Un;return Un<<=1,(Un&62914560)===0&&(Un=4194304),t}function ti(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function Da(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function xh(t,e,l,a,n,u){var c=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,h=t.expirationTimes,E=t.hiddenUpdates;for(l=c&~l;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var jh=/[\n"\\]/g;function ye(t){return t.replace(jh,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function ii(t,e,l,a,n,u,c,o){t.name="",c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"?t.type=c:t.removeAttribute("type"),e!=null?c==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+ge(e)):t.value!==""+ge(e)&&(t.value=""+ge(e)):c!=="submit"&&c!=="reset"||t.removeAttribute("value"),e!=null?ci(t,c,ge(e)):l!=null?ci(t,c,ge(l)):a!=null&&t.removeAttribute("value"),n==null&&u!=null&&(t.defaultChecked=!!u),n!=null&&(t.checked=n&&typeof n!="function"&&typeof n!="symbol"),o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?t.name=""+ge(o):t.removeAttribute("name")}function Xs(t,e,l,a,n,u,c,o){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),e!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||e!=null)){ui(t);return}l=l!=null?""+ge(l):"",e=e!=null?""+ge(e):l,o||e===t.value||(t.value=e),t.defaultValue=e}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=o?t.checked:!!a,t.defaultChecked=!!a,c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(t.name=c),ui(t)}function ci(t,e,l){e==="number"&&Bn(t.ownerDocument)===t||t.defaultValue===""+l||(t.defaultValue=""+l)}function kl(t,e,l,a){if(t=t.options,e){e={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),di=!1;if(Be)try{var qa={};Object.defineProperty(qa,"passive",{get:function(){di=!0}}),window.addEventListener("test",qa,qa),window.removeEventListener("test",qa,qa)}catch{di=!1}var tl=null,hi=null,Ln=null;function $s(){if(Ln)return Ln;var t,e=hi,l=e.length,a,n="value"in tl?tl.value:tl.textContent,u=n.length;for(t=0;t=Ya),ef=" ",lf=!1;function af(t,e){switch(t){case"keyup":return sm.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function nf(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var ta=!1;function rm(t,e){switch(t){case"compositionend":return nf(e);case"keypress":return e.which!==32?null:(lf=!0,ef);case"textInput":return t=e.data,t===ef&&lf?null:t;default:return null}}function om(t,e){if(ta)return t==="compositionend"||!Si&&af(t,e)?(t=$s(),Ln=hi=tl=null,ta=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:l,offset:e-t};t=a}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=hf(l)}}function gf(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?gf(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function yf(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=Bn(t.document);e instanceof t.HTMLIFrameElement;){try{var l=typeof e.contentWindow.location.href=="string"}catch{l=!1}if(l)t=e.contentWindow;else break;e=Bn(t.document)}return e}function Ti(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var pm=Be&&"documentMode"in document&&11>=document.documentMode,ea=null,Ai=null,Va=null,Ei=!1;function vf(t,e,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Ei||ea==null||ea!==Bn(a)||(a=ea,"selectionStart"in a&&Ti(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Va&&Qa(Va,a)||(Va=a,a=Ru(Ai,"onSelect"),0>=c,n-=c,Oe=1<<32-ie(e)+n|l<tt?(ct=Z,Z=null):ct=Z.sibling;var rt=z(v,Z,A[tt],R);if(rt===null){Z===null&&(Z=ct);break}t&&Z&&rt.alternate===null&&e(v,Z),m=u(rt,m,tt),ft===null?K=rt:ft.sibling=rt,ft=rt,Z=ct}if(tt===A.length)return l(v,Z),st&&Le(v,tt),K;if(Z===null){for(;tttt?(ct=Z,Z=null):ct=Z.sibling;var Al=z(v,Z,rt.value,R);if(Al===null){Z===null&&(Z=ct);break}t&&Z&&Al.alternate===null&&e(v,Z),m=u(Al,m,tt),ft===null?K=Al:ft.sibling=Al,ft=Al,Z=ct}if(rt.done)return l(v,Z),st&&Le(v,tt),K;if(Z===null){for(;!rt.done;tt++,rt=A.next())rt=U(v,rt.value,R),rt!==null&&(m=u(rt,m,tt),ft===null?K=rt:ft.sibling=rt,ft=rt);return st&&Le(v,tt),K}for(Z=a(Z);!rt.done;tt++,rt=A.next())rt=N(Z,v,tt,rt.value,R),rt!==null&&(t&&rt.alternate!==null&&Z.delete(rt.key===null?tt:rt.key),m=u(rt,m,tt),ft===null?K=rt:ft.sibling=rt,ft=rt);return t&&Z.forEach(function(wg){return e(v,wg)}),st&&Le(v,tt),K}function pt(v,m,A,R){if(typeof A=="object"&&A!==null&&A.type===L&&A.key===null&&(A=A.props.children),typeof A=="object"&&A!==null){switch(A.$$typeof){case O:t:{for(var K=A.key;m!==null;){if(m.key===K){if(K=A.type,K===L){if(m.tag===7){l(v,m.sibling),R=n(m,A.props.children),R.return=v,v=R;break t}}else if(m.elementType===K||typeof K=="object"&&K!==null&&K.$$typeof===Et&&ql(K)===m.type){l(v,m.sibling),R=n(m,A.props),ka(R,A),R.return=v,v=R;break t}l(v,m);break}else e(v,m);m=m.sibling}A.type===L?(R=Ul(A.props.children,v.mode,R,A.key),R.return=v,v=R):(R=Wn(A.type,A.key,A.props,null,v.mode,R),ka(R,A),R.return=v,v=R)}return c(v);case B:t:{for(K=A.key;m!==null;){if(m.key===K)if(m.tag===4&&m.stateNode.containerInfo===A.containerInfo&&m.stateNode.implementation===A.implementation){l(v,m.sibling),R=n(m,A.children||[]),R.return=v,v=R;break t}else{l(v,m);break}else e(v,m);m=m.sibling}R=Mi(A,v.mode,R),R.return=v,v=R}return c(v);case Et:return A=ql(A),pt(v,m,A,R)}if(_e(A))return X(v,m,A,R);if(Yt(A)){if(K=Yt(A),typeof K!="function")throw Error(f(150));return A=K.call(A),J(v,m,A,R)}if(typeof A.then=="function")return pt(v,m,eu(A),R);if(A.$$typeof===lt)return pt(v,m,Fn(v,A),R);lu(v,A)}return typeof A=="string"&&A!==""||typeof A=="number"||typeof A=="bigint"?(A=""+A,m!==null&&m.tag===6?(l(v,m.sibling),R=n(m,A),R.return=v,v=R):(l(v,m),R=Oi(A,v.mode,R),R.return=v,v=R),c(v)):l(v,m)}return function(v,m,A,R){try{$a=0;var K=pt(v,m,A,R);return da=null,K}catch(Z){if(Z===oa||Z===Pn)throw Z;var ft=se(29,Z,null,v.mode);return ft.lanes=R,ft.return=v,ft}}}var wl=Yf(!0),Gf=Yf(!1),ul=!1;function Xi(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Qi(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function il(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function cl(t,e,l){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(dt&2)!==0){var n=a.pending;return n===null?e.next=e:(e.next=n.next,n.next=e),a.pending=e,e=Jn(t),zf(t,null,l),e}return Kn(t,a,e,l),Jn(t)}function Fa(t,e,l){if(e=e.updateQueue,e!==null&&(e=e.shared,(l&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,Rs(t,l)}}function Vi(t,e){var l=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,u=null;if(l=l.firstBaseUpdate,l!==null){do{var c={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};u===null?n=u=c:u=u.next=c,l=l.next}while(l!==null);u===null?n=u=e:u=u.next=e}else n=u=e;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},t.updateQueue=l;return}t=l.lastBaseUpdate,t===null?l.firstBaseUpdate=e:t.next=e,l.lastBaseUpdate=e}var Zi=!1;function Ia(){if(Zi){var t=ra;if(t!==null)throw t}}function Pa(t,e,l,a){Zi=!1;var n=t.updateQueue;ul=!1;var u=n.firstBaseUpdate,c=n.lastBaseUpdate,o=n.shared.pending;if(o!==null){n.shared.pending=null;var h=o,E=h.next;h.next=null,c===null?u=E:c.next=E,c=h;var x=t.alternate;x!==null&&(x=x.updateQueue,o=x.lastBaseUpdate,o!==c&&(o===null?x.firstBaseUpdate=E:o.next=E,x.lastBaseUpdate=h))}if(u!==null){var U=n.baseState;c=0,x=E=h=null,o=u;do{var z=o.lane&-536870913,N=z!==o.lane;if(N?(it&z)===z:(a&z)===z){z!==0&&z===fa&&(Zi=!0),x!==null&&(x=x.next={lane:0,tag:o.tag,payload:o.payload,callback:null,next:null});t:{var X=t,J=o;z=e;var pt=l;switch(J.tag){case 1:if(X=J.payload,typeof X=="function"){U=X.call(pt,U,z);break t}U=X;break t;case 3:X.flags=X.flags&-65537|128;case 0:if(X=J.payload,z=typeof X=="function"?X.call(pt,U,z):X,z==null)break t;U=b({},U,z);break t;case 2:ul=!0}}z=o.callback,z!==null&&(t.flags|=64,N&&(t.flags|=8192),N=n.callbacks,N===null?n.callbacks=[z]:N.push(z))}else N={lane:z,tag:o.tag,payload:o.payload,callback:o.callback,next:null},x===null?(E=x=N,h=U):x=x.next=N,c|=z;if(o=o.next,o===null){if(o=n.shared.pending,o===null)break;N=o,o=N.next,N.next=null,n.lastBaseUpdate=N,n.shared.pending=null}}while(!0);x===null&&(h=U),n.baseState=h,n.firstBaseUpdate=E,n.lastBaseUpdate=x,u===null&&(n.shared.lanes=0),dl|=c,t.lanes=c,t.memoizedState=U}}function Xf(t,e){if(typeof t!="function")throw Error(f(191,t));t.call(e)}function Qf(t,e){var l=t.callbacks;if(l!==null)for(t.callbacks=null,t=0;tu?u:8;var c=M.T,o={};M.T=o,rc(t,!1,e,l);try{var h=n(),E=M.S;if(E!==null&&E(o,h),h!==null&&typeof h=="object"&&typeof h.then=="function"){var x=xm(h,a);ln(t,e,x,he(t))}else ln(t,e,a,he(t))}catch(U){ln(t,e,{then:function(){},status:"rejected",reason:U},he())}finally{Y.p=u,c!==null&&o.types!==null&&(c.types=o.types),M.T=c}}function jm(){}function sc(t,e,l,a){if(t.tag!==5)throw Error(f(476));var n=Tr(t).queue;br(t,n,e,$,l===null?jm:function(){return Ar(t),l(a)})}function Tr(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:$,baseState:$,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Xe,lastRenderedState:$},next:null};var l={};return e.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Xe,lastRenderedState:l},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Ar(t){var e=Tr(t);e.next===null&&(e=t.alternate.memoizedState),ln(t,e.next.queue,{},he())}function fc(){return Zt(bn)}function Er(){return Dt().memoizedState}function zr(){return Dt().memoizedState}function Hm(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var l=he();t=il(l);var a=cl(e,t,l);a!==null&&(ae(a,e,l),Fa(a,e,l)),e={cache:Li()},t.payload=e;return}e=e.return}}function Bm(t,e,l){var a=he();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},du(t)?Nr(e,l):(l=Ci(t,e,l,a),l!==null&&(ae(l,t,a),Cr(l,e,a)))}function _r(t,e,l){var a=he();ln(t,e,l,a)}function ln(t,e,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(du(t))Nr(e,n);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=e.lastRenderedReducer,u!==null))try{var c=e.lastRenderedState,o=u(c,l);if(n.hasEagerState=!0,n.eagerState=o,ce(o,c))return Kn(t,e,n,0),At===null&&Zn(),!1}catch{}if(l=Ci(t,e,n,a),l!==null)return ae(l,t,a),Cr(l,e,a),!0}return!1}function rc(t,e,l,a){if(a={lane:2,revertLane:Xc(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},du(t)){if(e)throw Error(f(479))}else e=Ci(t,l,a,2),e!==null&&ae(e,t,2)}function du(t){var e=t.alternate;return t===P||e!==null&&e===P}function Nr(t,e){ma=uu=!0;var l=t.pending;l===null?e.next=e:(e.next=l.next,l.next=e),t.pending=e}function Cr(t,e,l){if((l&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,Rs(t,l)}}var an={readContext:Zt,use:su,useCallback:xt,useContext:xt,useEffect:xt,useImperativeHandle:xt,useLayoutEffect:xt,useInsertionEffect:xt,useMemo:xt,useReducer:xt,useRef:xt,useState:xt,useDebugValue:xt,useDeferredValue:xt,useTransition:xt,useSyncExternalStore:xt,useId:xt,useHostTransitionStatus:xt,useFormState:xt,useActionState:xt,useOptimistic:xt,useMemoCache:xt,useCacheRefresh:xt};an.useEffectEvent=xt;var xr={readContext:Zt,use:su,useCallback:function(t,e){return kt().memoizedState=[t,e===void 0?null:e],t},useContext:Zt,useEffect:or,useImperativeHandle:function(t,e,l){l=l!=null?l.concat([t]):null,ru(4194308,4,gr.bind(null,e,t),l)},useLayoutEffect:function(t,e){return ru(4194308,4,t,e)},useInsertionEffect:function(t,e){ru(4,2,t,e)},useMemo:function(t,e){var l=kt();e=e===void 0?null:e;var a=t();if(Yl){Ie(!0);try{t()}finally{Ie(!1)}}return l.memoizedState=[a,e],a},useReducer:function(t,e,l){var a=kt();if(l!==void 0){var n=l(e);if(Yl){Ie(!0);try{l(e)}finally{Ie(!1)}}}else n=e;return a.memoizedState=a.baseState=n,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:n},a.queue=t,t=t.dispatch=Bm.bind(null,P,t),[a.memoizedState,t]},useRef:function(t){var e=kt();return t={current:t},e.memoizedState=t},useState:function(t){t=ac(t);var e=t.queue,l=_r.bind(null,P,e);return e.dispatch=l,[t.memoizedState,l]},useDebugValue:ic,useDeferredValue:function(t,e){var l=kt();return cc(l,t,e)},useTransition:function(){var t=ac(!1);return t=br.bind(null,P,t.queue,!0,!1),kt().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,l){var a=P,n=kt();if(st){if(l===void 0)throw Error(f(407));l=l()}else{if(l=e(),At===null)throw Error(f(349));(it&127)!==0||$f(a,e,l)}n.memoizedState=l;var u={value:l,getSnapshot:e};return n.queue=u,or(Ff.bind(null,a,u,t),[t]),a.flags|=2048,ya(9,{destroy:void 0},kf.bind(null,a,u,l,e),null),l},useId:function(){var t=kt(),e=At.identifierPrefix;if(st){var l=Me,a=Oe;l=(a&~(1<<32-ie(a)-1)).toString(32)+l,e="_"+e+"R_"+l,l=iu++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?c.createElement("select",{is:a.is}):c.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?c.createElement(n,{is:a.is}):c.createElement(n)}}u[Qt]=e,u[Ft]=a;t:for(c=e.child;c!==null;){if(c.tag===5||c.tag===6)u.appendChild(c.stateNode);else if(c.tag!==4&&c.tag!==27&&c.child!==null){c.child.return=c,c=c.child;continue}if(c===e)break t;for(;c.sibling===null;){if(c.return===null||c.return===e)break t;c=c.return}c.sibling.return=c.return,c=c.sibling}e.stateNode=u;t:switch(Jt(u,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&Ve(e)}}return _t(e),zc(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,l),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&Ve(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(f(166));if(t=at.current,ca(e)){if(t=e.stateNode,l=e.memoizedProps,a=null,n=Vt,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}t[Qt]=e,t=!!(t.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||Wo(t.nodeValue,l)),t||al(e,!0)}else t=Uu(t).createTextNode(a),t[Qt]=e,e.stateNode=t}return _t(e),null;case 31:if(l=e.memoizedState,t===null||t.memoizedState!==null){if(a=ca(e),l!==null){if(t===null){if(!a)throw Error(f(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[Qt]=e}else Dl(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;_t(e),t=!1}else l=ji(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=l),t=!0;if(!t)return e.flags&256?(re(e),e):(re(e),null);if((e.flags&128)!==0)throw Error(f(558))}return _t(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(n=ca(e),a!==null&&a.dehydrated!==null){if(t===null){if(!n)throw Error(f(318));if(n=e.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(f(317));n[Qt]=e}else Dl(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;_t(e),n=!1}else n=ji(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),n=!0;if(!n)return e.flags&256?(re(e),e):(re(e),null)}return re(e),(e.flags&128)!==0?(e.lanes=l,e):(l=a!==null,t=t!==null&&t.memoizedState!==null,l&&(a=e.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==n&&(a.flags|=2048)),l!==t&&l&&(e.child.flags|=8192),vu(e,e.updateQueue),_t(e),null);case 4:return Rt(),t===null&&Kc(e.stateNode.containerInfo),_t(e),null;case 10:return Ye(e.type),_t(e),null;case 19:if(j(Ut),a=e.memoizedState,a===null)return _t(e),null;if(n=(e.flags&128)!==0,u=a.rendering,u===null)if(n)un(a,!1);else{if(Ot!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(u=nu(t),u!==null){for(e.flags|=128,un(a,!1),t=u.updateQueue,e.updateQueue=t,vu(e,t),e.subtreeFlags=0,t=l,l=e.child;l!==null;)_f(l,t),l=l.sibling;return G(Ut,Ut.current&1|2),st&&Le(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&ne()>Au&&(e.flags|=128,n=!0,un(a,!1),e.lanes=4194304)}else{if(!n)if(t=nu(u),t!==null){if(e.flags|=128,n=!0,t=t.updateQueue,e.updateQueue=t,vu(e,t),un(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!st)return _t(e),null}else 2*ne()-a.renderingStartTime>Au&&l!==536870912&&(e.flags|=128,n=!0,un(a,!1),e.lanes=4194304);a.isBackwards?(u.sibling=e.child,e.child=u):(t=a.last,t!==null?t.sibling=u:e.child=u,a.last=u)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=ne(),t.sibling=null,l=Ut.current,G(Ut,n?l&1|2:l&1),st&&Le(e,a.treeForkCount),t):(_t(e),null);case 22:case 23:return re(e),Ji(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(l&536870912)!==0&&(e.flags&128)===0&&(_t(e),e.subtreeFlags&6&&(e.flags|=8192)):_t(e),l=e.updateQueue,l!==null&&vu(e,l.retryQueue),l=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==l&&(e.flags|=2048),t!==null&&j(Bl),null;case 24:return l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),Ye(Ht),_t(e),null;case 25:return null;case 30:return null}throw Error(f(156,e.tag))}function Gm(t,e){switch(Ui(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return Ye(Ht),Rt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return xn(e),null;case 31:if(e.memoizedState!==null){if(re(e),e.alternate===null)throw Error(f(340));Dl()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(re(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(f(340));Dl()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return j(Ut),null;case 4:return Rt(),null;case 10:return Ye(e.type),null;case 22:case 23:return re(e),Ji(),t!==null&&j(Bl),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return Ye(Ht),null;case 25:return null;default:return null}}function Ir(t,e){switch(Ui(e),e.tag){case 3:Ye(Ht),Rt();break;case 26:case 27:case 5:xn(e);break;case 4:Rt();break;case 31:e.memoizedState!==null&&re(e);break;case 13:re(e);break;case 19:j(Ut);break;case 10:Ye(e.type);break;case 22:case 23:re(e),Ji(),t!==null&&j(Bl);break;case 24:Ye(Ht)}}function cn(t,e){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&t)===t){a=void 0;var u=l.create,c=l.inst;a=u(),c.destroy=a}l=l.next}while(l!==n)}}catch(o){gt(e,e.return,o)}}function rl(t,e,l){try{var a=e.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var u=n.next;a=u;do{if((a.tag&t)===t){var c=a.inst,o=c.destroy;if(o!==void 0){c.destroy=void 0,n=e;var h=l,E=o;try{E()}catch(x){gt(n,h,x)}}}a=a.next}while(a!==u)}}catch(x){gt(e,e.return,x)}}function Pr(t){var e=t.updateQueue;if(e!==null){var l=t.stateNode;try{Qf(e,l)}catch(a){gt(t,t.return,a)}}}function to(t,e,l){l.props=Gl(t.type,t.memoizedProps),l.state=t.memoizedState;try{l.componentWillUnmount()}catch(a){gt(t,e,a)}}function sn(t,e){try{var l=t.ref;if(l!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof l=="function"?t.refCleanup=l(a):l.current=a}}catch(n){gt(t,e,n)}}function Re(t,e){var l=t.ref,a=t.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){gt(t,e,n)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){gt(t,e,n)}else l.current=null}function eo(t){var e=t.type,l=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break t;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){gt(t,t.return,n)}}function _c(t,e,l){try{var a=t.stateNode;fg(a,t.type,l,e),a[Ft]=e}catch(n){gt(t,t.return,n)}}function lo(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&vl(t.type)||t.tag===4}function Nc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||lo(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&vl(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Cc(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(t,e):(e=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,e.appendChild(t),l=l._reactRootContainer,l!=null||e.onclick!==null||(e.onclick=He));else if(a!==4&&(a===27&&vl(t.type)&&(l=t.stateNode,e=null),t=t.child,t!==null))for(Cc(t,e,l),t=t.sibling;t!==null;)Cc(t,e,l),t=t.sibling}function Su(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?l.insertBefore(t,e):l.appendChild(t);else if(a!==4&&(a===27&&vl(t.type)&&(l=t.stateNode),t=t.child,t!==null))for(Su(t,e,l),t=t.sibling;t!==null;)Su(t,e,l),t=t.sibling}function ao(t){var e=t.stateNode,l=t.memoizedProps;try{for(var a=t.type,n=e.attributes;n.length;)e.removeAttributeNode(n[0]);Jt(e,a,l),e[Qt]=t,e[Ft]=l}catch(u){gt(t,t.return,u)}}var Ze=!1,Lt=!1,xc=!1,no=typeof WeakSet=="function"?WeakSet:Set,Xt=null;function Xm(t,e){if(t=t.containerInfo,$c=wu,t=yf(t),Ti(t)){if("selectionStart"in t)var l={start:t.selectionStart,end:t.selectionEnd};else t:{l=(l=t.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{l.nodeType,u.nodeType}catch{l=null;break t}var c=0,o=-1,h=-1,E=0,x=0,U=t,z=null;e:for(;;){for(var N;U!==l||n!==0&&U.nodeType!==3||(o=c+n),U!==u||a!==0&&U.nodeType!==3||(h=c+a),U.nodeType===3&&(c+=U.nodeValue.length),(N=U.firstChild)!==null;)z=U,U=N;for(;;){if(U===t)break e;if(z===l&&++E===n&&(o=c),z===u&&++x===a&&(h=c),(N=U.nextSibling)!==null)break;U=z,z=U.parentNode}U=N}l=o===-1||h===-1?null:{start:o,end:h}}else l=null}l=l||{start:0,end:0}}else l=null;for(kc={focusedElem:t,selectionRange:l},wu=!1,Xt=e;Xt!==null;)if(e=Xt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Xt=t;else for(;Xt!==null;){switch(e=Xt,u=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(l=0;l title"))),Jt(u,a,l),u[Qt]=t,Gt(u),a=u;break t;case"link":var c=od("link","href",n).get(a+(l.href||""));if(c){for(var o=0;opt&&(c=pt,pt=J,J=c);var v=mf(o,J),m=mf(o,pt);if(v&&m&&(N.rangeCount!==1||N.anchorNode!==v.node||N.anchorOffset!==v.offset||N.focusNode!==m.node||N.focusOffset!==m.offset)){var A=U.createRange();A.setStart(v.node,v.offset),N.removeAllRanges(),J>pt?(N.addRange(A),N.extend(m.node,m.offset)):(A.setEnd(m.node,m.offset),N.addRange(A))}}}}for(U=[],N=o;N=N.parentNode;)N.nodeType===1&&U.push({element:N,left:N.scrollLeft,top:N.scrollTop});for(typeof o.focus=="function"&&o.focus(),o=0;ol?32:l,M.T=null,l=Hc,Hc=null;var u=ml,c=ke;if(wt=0,Ta=ml=null,ke=0,(dt&6)!==0)throw Error(f(331));var o=dt;if(dt|=4,yo(u.current),ho(u,u.current,c,l),dt=o,mn(0,!1),ue&&typeof ue.onPostCommitFiberRoot=="function")try{ue.onPostCommitFiberRoot(Ra,u)}catch{}return!0}finally{Y.p=n,M.T=a,jo(t,e)}}function Bo(t,e,l){e=Se(l,e),e=mc(t.stateNode,e,2),t=cl(t,e,2),t!==null&&(Da(t,2),Ue(t))}function gt(t,e,l){if(t.tag===3)Bo(t,t,l);else for(;e!==null;){if(e.tag===3){Bo(e,t,l);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(hl===null||!hl.has(a))){t=Se(l,t),l=Br(2),a=cl(e,l,2),a!==null&&(qr(l,a,e,t),Da(a,2),Ue(a));break}}e=e.return}}function wc(t,e,l){var a=t.pingCache;if(a===null){a=t.pingCache=new Zm;var n=new Set;a.set(e,n)}else n=a.get(e),n===void 0&&(n=new Set,a.set(e,n));n.has(l)||(Rc=!0,n.add(l),t=km.bind(null,t,e,l),e.then(t,t))}function km(t,e,l){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&l,t.warmLanes&=~l,At===t&&(it&l)===l&&(Ot===4||Ot===3&&(it&62914560)===it&&300>ne()-Tu?(dt&2)===0&&Aa(t,0):Uc|=l,ba===it&&(ba=0)),Ue(t)}function qo(t,e){e===0&&(e=Os()),t=Rl(t,e),t!==null&&(Da(t,e),Ue(t))}function Fm(t){var e=t.memoizedState,l=0;e!==null&&(l=e.retryLane),qo(t,l)}function Im(t,e){var l=0;switch(t.tag){case 31:case 13:var a=t.stateNode,n=t.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(e),qo(t,l)}function Pm(t,e){return Iu(t,e)}var xu=null,za=null,Yc=!1,Ou=!1,Gc=!1,yl=0;function Ue(t){t!==za&&t.next===null&&(za===null?xu=za=t:za=za.next=t),Ou=!0,Yc||(Yc=!0,eg())}function mn(t,e){if(!Gc&&Ou){Gc=!0;do for(var l=!1,a=xu;a!==null;){if(t!==0){var n=a.pendingLanes;if(n===0)var u=0;else{var c=a.suspendedLanes,o=a.pingedLanes;u=(1<<31-ie(42|t)+1)-1,u&=n&~(c&~o),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(l=!0,Go(a,u))}else u=it,u=Dn(a,a===At?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||Ua(a,u)||(l=!0,Go(a,u));a=a.next}while(l);Gc=!1}}function tg(){Lo()}function Lo(){Ou=Yc=!1;var t=0;yl!==0&&og()&&(t=yl);for(var e=ne(),l=null,a=xu;a!==null;){var n=a.next,u=wo(a,e);u===0?(a.next=null,l===null?xu=n:l.next=n,n===null&&(za=l)):(l=a,(t!==0||(u&3)!==0)&&(Ou=!0)),a=n}wt!==0&&wt!==5||mn(t),yl!==0&&(yl=0)}function wo(t,e){for(var l=t.suspendedLanes,a=t.pingedLanes,n=t.expirationTimes,u=t.pendingLanes&-62914561;0o)break;var x=h.transferSize,U=h.initiatorType;x&&$o(U)&&(h=h.responseEnd,c+=x*(h"u"?null:document;function cd(t,e,l){var a=_a;if(a&&typeof e=="string"&&e){var n=ye(e);n='link[rel="'+t+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),id.has(n)||(id.add(n),t={rel:t,crossOrigin:l,href:e},a.querySelector(n)===null&&(e=a.createElement("link"),Jt(e,"link",t),Gt(e),a.head.appendChild(e)))}}function bg(t){Fe.D(t),cd("dns-prefetch",t,null)}function Tg(t,e){Fe.C(t,e),cd("preconnect",t,e)}function Ag(t,e,l){Fe.L(t,e,l);var a=_a;if(a&&t&&e){var n='link[rel="preload"][as="'+ye(e)+'"]';e==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+ye(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+ye(l.imageSizes)+'"]')):n+='[href="'+ye(t)+'"]';var u=n;switch(e){case"style":u=Na(t);break;case"script":u=Ca(t)}ze.has(u)||(t=b({rel:"preload",href:e==="image"&&l&&l.imageSrcSet?void 0:t,as:e},l),ze.set(u,t),a.querySelector(n)!==null||e==="style"&&a.querySelector(Sn(u))||e==="script"&&a.querySelector(pn(u))||(e=a.createElement("link"),Jt(e,"link",t),Gt(e),a.head.appendChild(e)))}}function Eg(t,e){Fe.m(t,e);var l=_a;if(l&&t){var a=e&&typeof e.as=="string"?e.as:"script",n='link[rel="modulepreload"][as="'+ye(a)+'"][href="'+ye(t)+'"]',u=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=Ca(t)}if(!ze.has(u)&&(t=b({rel:"modulepreload",href:t},e),ze.set(u,t),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(pn(u)))return}a=l.createElement("link"),Jt(a,"link",t),Gt(a),l.head.appendChild(a)}}}function zg(t,e,l){Fe.S(t,e,l);var a=_a;if(a&&t){var n=Wl(a).hoistableStyles,u=Na(t);e=e||"default";var c=n.get(u);if(!c){var o={loading:0,preload:null};if(c=a.querySelector(Sn(u)))o.loading=5;else{t=b({rel:"stylesheet",href:t,"data-precedence":e},l),(l=ze.get(u))&&as(t,l);var h=c=a.createElement("link");Gt(h),Jt(h,"link",t),h._p=new Promise(function(E,x){h.onload=E,h.onerror=x}),h.addEventListener("load",function(){o.loading|=1}),h.addEventListener("error",function(){o.loading|=2}),o.loading|=4,ju(c,e,a)}c={type:"stylesheet",instance:c,count:1,state:o},n.set(u,c)}}}function _g(t,e){Fe.X(t,e);var l=_a;if(l&&t){var a=Wl(l).hoistableScripts,n=Ca(t),u=a.get(n);u||(u=l.querySelector(pn(n)),u||(t=b({src:t,async:!0},e),(e=ze.get(n))&&ns(t,e),u=l.createElement("script"),Gt(u),Jt(u,"link",t),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function Ng(t,e){Fe.M(t,e);var l=_a;if(l&&t){var a=Wl(l).hoistableScripts,n=Ca(t),u=a.get(n);u||(u=l.querySelector(pn(n)),u||(t=b({src:t,async:!0,type:"module"},e),(e=ze.get(n))&&ns(t,e),u=l.createElement("script"),Gt(u),Jt(u,"link",t),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function sd(t,e,l,a){var n=(n=at.current)?Du(n):null;if(!n)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(e=Na(l.href),l=Wl(n).hoistableStyles,a=l.get(e),a||(a={type:"style",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){t=Na(l.href);var u=Wl(n).hoistableStyles,c=u.get(t);if(c||(n=n.ownerDocument||n,c={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,c),(u=n.querySelector(Sn(t)))&&!u._p&&(c.instance=u,c.state.loading=5),ze.has(t)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},ze.set(t,l),u||Cg(n,t,l,c.state))),e&&a===null)throw Error(f(528,""));return c}if(e&&a!==null)throw Error(f(529,""));return null;case"script":return e=l.async,l=l.src,typeof l=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=Ca(l),l=Wl(n).hoistableScripts,a=l.get(e),a||(a={type:"script",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Na(t){return'href="'+ye(t)+'"'}function Sn(t){return'link[rel="stylesheet"]['+t+"]"}function fd(t){return b({},t,{"data-precedence":t.precedence,precedence:null})}function Cg(t,e,l,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),Jt(e,"link",l),Gt(e),t.head.appendChild(e))}function Ca(t){return'[src="'+ye(t)+'"]'}function pn(t){return"script[async]"+t}function rd(t,e,l){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+ye(l.href)+'"]');if(a)return e.instance=a,Gt(a),a;var n=b({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Gt(a),Jt(a,"style",n),ju(a,l.precedence,t),e.instance=a;case"stylesheet":n=Na(l.href);var u=t.querySelector(Sn(n));if(u)return e.state.loading|=4,e.instance=u,Gt(u),u;a=fd(l),(n=ze.get(n))&&as(a,n),u=(t.ownerDocument||t).createElement("link"),Gt(u);var c=u;return c._p=new Promise(function(o,h){c.onload=o,c.onerror=h}),Jt(u,"link",a),e.state.loading|=4,ju(u,l.precedence,t),e.instance=u;case"script":return u=Ca(l.src),(n=t.querySelector(pn(u)))?(e.instance=n,Gt(n),n):(a=l,(n=ze.get(u))&&(a=b({},l),ns(a,n)),t=t.ownerDocument||t,n=t.createElement("script"),Gt(n),Jt(n,"link",a),t.head.appendChild(n),e.instance=n);case"void":return null;default:throw Error(f(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,ju(a,l.precedence,t));return e.instance}function ju(t,e,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,u=n,c=0;c title"):null)}function xg(t,e,l){if(l===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;return e.rel==="stylesheet"?(t=e.disabled,typeof e.precedence=="string"&&t==null):!0;case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function hd(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Og(t,e,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=Na(a.href),u=e.querySelector(Sn(n));if(u){e=u._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=Bu.bind(t),e.then(t,t)),l.state.loading|=4,l.instance=u,Gt(u);return}u=e.ownerDocument||e,a=fd(a),(n=ze.get(n))&&as(a,n),u=u.createElement("link"),Gt(u);var c=u;c._p=new Promise(function(o,h){c.onload=o,c.onerror=h}),Jt(u,"link",a),l.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(l,e),(e=l.state.preload)&&(l.state.loading&3)===0&&(t.count++,l=Bu.bind(t),e.addEventListener("load",l),e.addEventListener("error",l))}}var us=0;function Mg(t,e){return t.stylesheets&&t.count===0&&Lu(t,t.stylesheets),0us?50:800)+e);return t.unsuspend=l,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function Bu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Lu(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var qu=null;function Lu(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,qu=new Map,e.forEach(Rg,t),qu=null,Bu.call(t))}function Rg(t,e){if(!(e.state.loading&4)){var l=qu.get(t);if(l)var a=l.get(null);else{l=new Map,qu.set(t,l);for(var n=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(s){console.error(s)}}return i(),ms.exports=Jg(),ms.exports}var $g=Wg();const kg=4294967295;function Nn({isInputDisabled:i,controlReady:s}){return!i&&s}function qd(i){if(!Number.isInteger(i.pointerId)||i.pointerId<0||i.pointerId>kg)throw new RangeError("pointerId must be an integer from 0 to 4294967295");return{type:"touch",...i}}function Fg(i){return{type:"systemKey",key:i}}function Ig({screenWidth:i,screenHeight:s,maxWidth:r,maxHeight:f}){const d=Math.min(r/i,f/s),p=i*d,T=s*d;return{width:p,height:T,offsetX:(r-p)/2,offsetY:(f-T)/2,scale:d}}function Pg({fit:i,x:s,y:r}){const f=s-i.offsetX,d=r-i.offsetY;return f<0||d<0||f>i.width||d>i.height?null:{x:f/i.scale,y:d/i.scale}}function ty({bounds:i,fit:s,markerIndexForWidget:r,markerSize:f,padding:d}){const p={left:i.x*s.scale,top:i.y*s.scale,width:i.width*s.scale,height:i.height*s.scale},T=p.left+p.width,_=p.top+p.height,C=r*(f+d),g=T-f-d-C,D=p.top+d,b=p.left+(p.width-f)/2,H=p.top+(p.height-f)/2;return{left:Math.round(Ld(p.width>=f+d*2?g:b,p.left,T-f)),top:Math.round(Ld(p.height>=f+d*2?D:H,p.top,_-f))}}function Ld(i,s,r){return s>r?(s+r)/2:Math.min(Math.max(i,s),r)}function ey({cancelAnimationFrame:i,onRendererChange:s,requestAnimationFrame:r}){let f=null,d=null,p=null;const T=()=>{if(!d)return;const _=d;d=null,f=null,s(null),_.close()};return{close(){T()},getRenderer(){return d},resize(_){p=_,d?.resize(_)},setCanvas(_){_!==f&&(T(),f=_,_&&(d=ly({canvas:_,cancelAnimationFrame:i,requestAnimationFrame:r}),p&&d.resize(p),s(d)))}}}function ly({canvas:i,cancelAnimationFrame:s,requestAnimationFrame:r}){const f=r??globalThis.requestAnimationFrame.bind(globalThis),d=s??globalThis.cancelAnimationFrame.bind(globalThis);let p=i.width,T=i.height,_=null,C=null;const g=()=>{C=null;const D=_;_=null,D&&ay({canvas:i,screenHeight:T,screenWidth:p,videoFrame:D})};return{close(){C!==null&&(d(C),C=null),_?.close?.(),_=null},render(D){_?.close?.(),_=D,C===null&&(C=f(g))},resize(D){p=D.screenWidth,T=D.screenHeight}}}function ay({canvas:i,screenHeight:s,screenWidth:r,videoFrame:f}){const d=i.getContext("2d");if(!d)return f.close?.(),!1;i.width=r,i.height=s;try{return d.drawImage(f,0,0,r,s),!0}finally{f.close?.()}}const ps=26,ny=4;function wd({isInputDisabled:i,onDeviceControlMessage:s,onDeviceVideoRendererChange:r,overlayMarkers:f=[],surfaceState:d}){const p=q.useRef(null),T=q.useRef(null),_=q.useRef(null),C=q.useRef(0),[g,D]=q.useState({width:0,height:0}),b=d.metadata,H=`${b.deviceId}:${b.screenWidth}:${b.screenHeight}`;T.current||(T.current=ey({onRendererChange:r})),q.useEffect(()=>{const w=p.current;if(!w)return;const et=()=>{const bt=w.getBoundingClientRect();D({width:bt.width,height:bt.height})};et();const lt=new ResizeObserver(et);return lt.observe(w),()=>lt.disconnect()},[]),q.useEffect(()=>{const w=_.current;w&&(s(qd({action:"cancel",pointerId:w.pointerId,x:w.x,y:w.y,screenWidth:w.screenWidth,screenHeight:w.screenHeight})),_.current=null)},[H,s]);const O=q.useMemo(()=>g.width<=0||g.height<=0?null:Ig({screenWidth:b.screenWidth,screenHeight:b.screenHeight,maxWidth:g.width,maxHeight:g.height}),[g.height,g.width,b.screenHeight,b.screenWidth]),B=q.useCallback(w=>{T.current?.setCanvas(w)},[]);q.useEffect(()=>{T.current?.resize({screenHeight:b.screenHeight,screenWidth:b.screenWidth})},[b.screenHeight,b.screenWidth]),q.useEffect(()=>{const w=T.current;return()=>{w?.close()}},[]);const L=(w,et)=>{if(!Nn({isInputDisabled:i,controlReady:b.controlReady})||!O)return;if(w==="move"){const Nt=performance.now();if(Nt-C.current<16)return;C.current=Nt}const lt=et.pointerId,bt=et.currentTarget.getBoundingClientRect();let F=Pg({fit:O,x:et.clientX-bt.left,y:et.clientY-bt.top}),ot=b.screenWidth,V=b.screenHeight;if(!F){const Nt=_.current;if(w!=="up"&&w!=="cancel"||Nt?.pointerId!==lt)return;F={x:Nt.x,y:Nt.y},ot=Nt.screenWidth,V=Nt.screenHeight}const Et=qd({action:w,pointerId:lt,x:F.x,y:F.y,screenWidth:ot,screenHeight:V});s(Et),w==="down"?(_.current={pointerId:lt,x:F.x,y:F.y,screenWidth:ot,screenHeight:V},et.currentTarget.setPointerCapture(lt)):w==="up"||w==="cancel"?(_.current?.pointerId===lt&&(_.current=null),et.currentTarget.hasPointerCapture(lt)&&et.currentTarget.releasePointerCapture(lt)):_.current?.pointerId===lt&&(_.current={pointerId:lt,x:F.x,y:F.y,screenWidth:ot,screenHeight:V})},W=w=>{Nn({isInputDisabled:i,controlReady:b.controlReady})&&s(Fg(w))};return S.jsxs("div",{className:"device-shell",children:[S.jsx("div",{className:"device-view-area",onPointerCancel:w=>L("cancel",w),onPointerDown:w=>L("down",w),onPointerMove:w=>L("move",w),onPointerUp:w=>L("up",w),ref:p,children:O?S.jsxs("div",{className:"device-view-frame",style:{height:`${O.height}px`,transform:`translate(${O.offsetX}px, ${O.offsetY}px)`,width:`${O.width}px`},title:b.deviceId,children:[S.jsx("canvas",{"aria-label":"Device video",className:"device-view-canvas",ref:B}),f.length>0?S.jsx("div",{"aria-label":"Selection Comment markers",className:"selection-marker-layer",children:uy(f,O).map(({marker:w,placement:et})=>S.jsx("div",{className:"selection-marker",style:{height:`${ps}px`,left:`${et.left}px`,top:`${et.top}px`,width:`${ps}px`},title:w.widgetLabel,children:w.number},w.id))}):null,d.status==="waitingForVideo"?S.jsxs(S.Fragment,{children:[S.jsx("div",{className:"device-view-status",children:"Waiting for video"}),S.jsx("div",{className:"device-view-device-id",children:b.deviceId})]}):null]}):null}),S.jsxs("div",{className:"surface-controls",children:[S.jsx("button",{"aria-label":"Back",disabled:!Nn({isInputDisabled:i,controlReady:b.controlReady}),onClick:()=>W("back"),title:"Back",type:"button",children:S.jsx("span",{"aria-hidden":"true",className:"surface-control-icon surface-control-icon-back"})}),S.jsx("button",{"aria-label":"Home",disabled:!Nn({isInputDisabled:i,controlReady:b.controlReady}),onClick:()=>W("home"),title:"Home",type:"button",children:S.jsx("span",{"aria-hidden":"true",className:"surface-control-icon surface-control-icon-home"})}),S.jsx("button",{"aria-label":"Recents",disabled:!Nn({isInputDisabled:i,controlReady:b.controlReady}),onClick:()=>W("recents"),title:"Recents",type:"button",children:S.jsx("span",{"aria-hidden":"true",className:"surface-control-icon surface-control-icon-recents"})})]})]})}function uy(i,s){const r=new Map;return i.map(f=>{const d=r.get(f.widgetId)??0;return r.set(f.widgetId,d+1),{marker:f,placement:ty({bounds:f.bounds,fit:s,markerIndexForWidget:d,markerSize:ps,padding:ny})}})}function Yd(i,s){return i.status==="connecting"?{label:"Connecting device",detail:`Preparing ${s.topBarLabel} screen stream`,title:"Connecting device",retryable:!1}:i.status==="failed"?{label:i.message,detail:s.surfaceLabel,title:i.message,retryable:!0}:i.status==="waitingForVideo"?{label:"Waiting for video",detail:`Preparing ${i.metadata.deviceId} video stream`,title:i.metadata.deviceId,retryable:!1}:{label:s.surfaceLabel,title:s.title,retryable:!1}}function iy({isInputDisabled:i,isSelectWidgetActive:s,overlayMarkers:r,onDeviceControlMessage:f,onDeviceVideoRendererChange:d,onRetry:p,surfaceState:T,targetDeviceDisplay:_}){return S.jsx("section",{className:`workbench-panel live-app-surface ${s?"live-app-surface-selecting":""}`,children:T.status==="renderingVideo"?S.jsx("div",{className:"live-app-device-stage",children:S.jsx(wd,{isInputDisabled:i,onDeviceControlMessage:f,onDeviceVideoRendererChange:d,overlayMarkers:r,surfaceState:T})}):T.status==="waitingForVideo"?S.jsxs("div",{className:"live-app-surface-waiting-stack",children:[S.jsx("div",{className:"live-app-surface-hidden-device-shell","aria-hidden":"true",children:S.jsx(wd,{isInputDisabled:i,onDeviceControlMessage:f,onDeviceVideoRendererChange:d,surfaceState:T})}),S.jsx(Gd,{content:Yd(T,_),onRetry:p})]}):S.jsx(Gd,{content:Yd(T,_),onRetry:p})})}function Gd({content:i,onRetry:s}){return S.jsx("div",{className:"live-app-phone-state-shell",title:i.title,children:S.jsxs("div",{className:"live-app-phone-state-hardware",children:[S.jsx("div",{className:"live-app-phone-state-speaker"}),S.jsxs("div",{className:"live-app-phone-state-screen",children:[S.jsxs("div",{className:"live-app-phone-status-bar",children:[S.jsx("span",{children:"9:41"}),S.jsx("span",{children:"Ask UI"})]}),S.jsxs("div",{className:"live-app-phone-skeleton",children:[S.jsx("div",{className:"live-app-phone-skeleton-hero"}),S.jsx("div",{className:"live-app-phone-skeleton-line live-app-phone-skeleton-line-wide"}),S.jsx("div",{className:"live-app-phone-skeleton-line"}),S.jsxs("div",{className:"live-app-phone-skeleton-grid",children:[S.jsx("div",{}),S.jsx("div",{}),S.jsx("div",{}),S.jsx("div",{})]})]}),S.jsxs("div",{className:"live-app-phone-state-overlay",children:[S.jsx("div",{className:"live-app-phone-spinner"}),S.jsx("div",{className:"live-app-phone-state-title",children:i.label}),i.detail?S.jsx("div",{className:"live-app-phone-state-detail",children:i.detail}):null,i.retryable?S.jsx("button",{className:"live-app-surface-retry",onClick:s,type:"button",children:"Retry"}):null]})]})]})})}function cy(i){return{status:"ready",agentStatus:i.agentStatus,readOnly:i.readOnly,connectionWarning:null,messages:i.messages}}function sy(i,s){return s.reduce(eh,cy(i))}function eh(i,s){return i.status!=="ready"?i:s.type==="chat_snapshot"?{...i,agentStatus:s.payload.agentStatus,messages:s.payload.messages,connectionWarning:null}:s.type==="agent_status_changed"?{...i,agentStatus:s.payload.agentStatus,connectionWarning:null}:s.type==="chat_history_changed"?{...i,messages:s.payload.messages,connectionWarning:null}:i}function fy(i){return i.status!=="ready"?i:{...i,agentStatus:"waiting_for_agent",connectionWarning:"Bridge session events disconnected."}}function lh(i){return i==="agent_ready"?"Agent ready":i==="agent_working"?"Agent working":"Waiting for agent"}function ry(i){return i.status!=="ready"?[]:i.agentStatus!=="agent_working"?i.messages:[...i.messages,{id:"agent-working-placeholder",role:"agent",text:"Agent working..."}]}function oy(i){return(i.parts??[]).filter(s=>s.type==="selection_comment").map((s,r)=>({id:s.attachment.id,number:r+1,widgetLabel:s.attachment.selectedWidget.displayLabel,text:s.attachment.commentText,sourceLocation:s.attachment.selectedWidget.sourceLocation??null,snapshotLabel:s.attachment.snapshot.status==="available"?"Snapshot available":"Snapshot unavailable"}))}function dy(i){const s=oy(i).map(r=>({type:"selection_comment",summary:r}));return i.text.trim().length===0?s:[...s,{type:"text",text:i.text}]}const Xd=4e3;function hy(i,s,r=!1,f=0){const d=s.length>Xd;return i.status!=="ready"?{canSend:!1,disabledReason:"Chat is not connected.",isTooLong:d}:i.readOnly?{canSend:!1,disabledReason:"Read-only browser tabs cannot send Chat messages.",isTooLong:d}:i.agentStatus!=="agent_ready"?{canSend:!1,disabledReason:`Agent Status is ${lh(i.agentStatus)}.`,isTooLong:d}:r?{canSend:!1,disabledReason:"Sending...",isTooLong:d}:s.trim().length===0&&f===0?{canSend:!1,disabledReason:"Type a message to send.",isTooLong:d}:d?{canSend:!1,disabledReason:`Message must be ${Xd} characters or fewer.`,isTooLong:d}:{canSend:!0,disabledReason:null,isTooLong:d}}function my(){return{maxLength:void 0}}function gy(i,s){return i==="Enter"&&!s}function yy(i,s,r=i){return s&&i===r?"":i}function vy({attachmentTokens:i,chatSessionState:s,composer:r,onAttachmentTokenClick:f,placeholder:d,sessionId:p}){const T=my();return S.jsxs("form",{className:"chat-composer","aria-label":"Chat composer",onSubmit:r.handleSubmit,children:[i.length>0?S.jsx("div",{className:"attachment-token-list","aria-label":"Selection Comment attachments",children:i.map(_=>S.jsxs("button",{"aria-label":`Open Selection Comment attachment ${_.number}`,className:`attachment-token ${_.isLocatable?"":"attachment-token-unavailable"}`,onClick:()=>f(_),type:"button",children:[S.jsxs("span",{className:"attachment-token-number",children:["#",_.number]}),S.jsx("span",{className:"attachment-token-label",children:_.widgetLabel})]},_.id))}):null,S.jsx("textarea",{"aria-label":"Message",className:"chat-composer-input",disabled:s.status==="ready"&&s.readOnly,maxLength:T.maxLength,onChange:_=>{r.handleComposerTextChange(_.target.value)},onKeyDown:r.handleComposerKeyDown,placeholder:d,ref:r.composerInputRef,rows:3,value:r.composerText}),S.jsxs("div",{className:"chat-composer-footer",children:[S.jsx("span",{className:"chat-composer-disabled-reason",children:r.sendError??r.composerDisabledReason}),S.jsx("button",{className:"chat-send-button",disabled:!r.composerState.canSend||p===null,type:"submit",children:"Send"})]})]})}const Sy=48;function py(i){return i.scrollHeight-i.clientHeight-i.scrollTop<=Sy}function by({isNearBottom:i,messageCountChanged:s}){return s?{shouldScrollToBottom:i,showNewMessageIndicator:!i}:{shouldScrollToBottom:!1,showNewMessageIndicator:!1}}function Ty({isNearBottom:i,showNewMessageIndicator:s}){return{showNewMessageIndicator:i?!1:s}}function Ay({chatSessionState:i,emptyState:s,title:r,visibleMessages:f}){const d=q.useRef(null),p=q.useRef(f.length),T=q.useRef(!0),[_,C]=q.useState(!1);q.useEffect(()=>{const b=p.current!==f.length,H=by({isNearBottom:T.current,messageCountChanged:b});p.current=f.length,H.shouldScrollToBottom&&requestAnimationFrame(()=>{const O=d.current;O!==null&&(O.scrollTop=O.scrollHeight)}),H.showNewMessageIndicator?C(!0):(H.shouldScrollToBottom||!b)&&C(!1)},[f.length]);function g(){const b=d.current;b!==null&&(T.current=py({clientHeight:b.clientHeight,scrollHeight:b.scrollHeight,scrollTop:b.scrollTop}),C(Ty({isNearBottom:T.current,showNewMessageIndicator:_}).showNewMessageIndicator))}function D(){const b=d.current;b!==null&&(b.scrollTop=b.scrollHeight,T.current=!0,C(!1))}return S.jsxs("section",{className:"chat-history","aria-labelledby":"chat-history-title",onScroll:g,ref:d,children:[S.jsx("div",{id:"chat-history-title",className:"chat-section-title",children:r}),i.status==="ready"&&i.connectionWarning?S.jsx("div",{className:"chat-connection-warning",children:i.connectionWarning}):null,i.status==="ready"&&f.length>0?S.jsx("ol",{className:"chat-history-list",children:f.map(b=>S.jsxs("li",{className:b.id==="agent-working-placeholder"?"chat-history-message chat-history-message-working":"chat-history-message",children:[S.jsx("div",{className:"chat-history-message-role",children:b.role}),S.jsx(Ey,{message:b})]},b.id))}):S.jsx("div",{className:"chat-history-empty",children:i.status==="loading"?"Loading Chat History...":s}),_?S.jsx("button",{className:"chat-history-new-message",onClick:D,type:"button",children:"New messages"}):null]})}function Ey({message:i}){const s=dy(i),r=s.filter(d=>d.type==="selection_comment"),f=s.find(d=>d.type==="text");return S.jsxs(S.Fragment,{children:[r.length>0?S.jsx("ol",{"aria-label":"Selection Comment attachments",className:"chat-history-attachment-list",children:r.map(d=>S.jsxs("li",{className:"chat-history-attachment",children:[S.jsxs("div",{className:"chat-history-attachment-heading",children:[S.jsxs("span",{className:"chat-history-attachment-number",children:["#",d.summary.number]}),S.jsx("span",{className:"chat-history-attachment-widget",children:d.summary.widgetLabel})]}),S.jsx("div",{className:"chat-history-attachment-text",children:d.summary.text}),S.jsxs("div",{className:"chat-history-attachment-meta",children:[d.summary.sourceLocation?S.jsx("span",{children:d.summary.sourceLocation}):null,S.jsx("span",{children:d.summary.snapshotLabel})]})]},d.summary.id))}):null,f?S.jsx("div",{className:"chat-history-message-text",children:f.text}):null]})}function zy({agentStatusLabel:i,agentStatusValue:s,title:r}){return S.jsxs("header",{className:"chat-panel-header",children:[S.jsx("div",{className:"chat-panel-title",children:r}),S.jsxs("div",{className:"agent-status","aria-label":i,children:[S.jsx("span",{className:"agent-status-dot","aria-hidden":"true"}),S.jsx("span",{className:"agent-status-label",children:i}),S.jsx("span",{className:"agent-status-value",children:s})]})]})}function _y(i,s){if(s===null)return null;const r=uh(i,s);return r===null?null:{id:Cn(r.id)??"",displayLabel:Cn(r.label)??"",sourceLocation:Cn(r.sourceLocation),visibleText:Cn(r.visibleText),semanticInfo:Cn(r.semanticInfo)}}function Ny(i){const s=new Set;return ah(i,s),s}function Cy(i){const s=new Map;return nh(i,s),s}function ah(i,s){s.add(i.id);const r=Array.isArray(i.children)?i.children:[];for(const f of r)ah(f,s)}function nh(i,s){xy(i.bounds)&&s.set(i.id,i.bounds);const r=Array.isArray(i.children)?i.children:[];for(const f of r)nh(f,s)}function xy(i){return i!==void 0&&Number.isFinite(i.x)&&Number.isFinite(i.y)&&Number.isFinite(i.width)&&Number.isFinite(i.height)&&i.width>0&&i.height>0}function Cn(i){if(i!=null){if(typeof i=="string")return i;if(typeof i=="number"||typeof i=="boolean"||typeof i=="bigint")return String(i);try{return JSON.stringify(i)}catch{return String(i)}}}function uh(i,s){if(i.id===s)return i;const r=Array.isArray(i.children)?i.children:[];for(const f of r){const d=uh(f,s);if(d!==null)return d}return null}function Oy(i,s){return i.comments.map((r,f)=>({id:r.id,number:f+1,widgetId:r.widgetId,widgetLabel:r.widgetLabel,isLocatable:s?.has(r.widgetId)??!0}))}function My({isSelectWidgetActive:i,locatableWidgetBoundsById:s,state:r}){return i?r.comments.map((f,d)=>{const p=s.get(f.widgetId);return p===void 0?null:{id:f.id,number:d+1,widgetId:f.widgetId,widgetLabel:f.widgetLabel,bounds:p}}).filter(f=>f!==null):[]}function Ry(i,s){return i.comments.find(r=>r.id===s)??null}function Uy(i,s){return s===null?i:{id:s.widgetId,displayLabel:s.widgetLabel,...s.sourceLocation?{sourceLocation:s.sourceLocation}:{},...s.visibleText?{visibleText:s.visibleText}:{},...s.semanticInfo?{semanticInfo:s.semanticInfo}:{}}}function Dy(i,s){return s===null?[]:i.comments.filter(r=>r.widgetId===s.id)}function jy(i,s){return Dy(i,s).map((r,f)=>({number:f+1,...r}))}const Qd=1e3,Vd=20;function Hy({isReadOnly:i=!1,isSelectWidgetActive:s,selectedWidget:r,widgetTreeStatus:f,text:d,batchSize:p}){const T=d.length>Qd;return i?{canAdd:!1,disabledReason:"Read-only browser tabs cannot edit Selection Comments.",isTooLong:T}:s?f==="error"?{canAdd:!1,disabledReason:"Widget Tree is unavailable.",isTooLong:T}:r===null||r.id.trim().length===0||r.displayLabel.trim().length===0?{canAdd:!1,disabledReason:"Select a widget with a reliable label.",isTooLong:T}:d.trim().length===0?{canAdd:!1,disabledReason:"Type a Selection Comment.",isTooLong:T}:T?{canAdd:!1,disabledReason:`Selection Comment must be ${Qd} characters or fewer.`,isTooLong:T}:p>=Vd?{canAdd:!1,disabledReason:`One batch can include ${Vd} Selection Comments.`,isTooLong:T}:{canAdd:!0,disabledReason:null,isTooLong:T}:{canAdd:!1,disabledReason:"Select Widget mode is required.",isTooLong:T}}function By(){return{maxLength:void 0}}function qy(){return{comments:[],draftsByWidgetId:{},nextCommentId:1}}function Ly(i,s,r,f={}){if(r.length===0)return i;const d=new Map(r.map(T=>[T.id,T])),p=Object.entries(f);return{...i,comments:i.comments.filter(T=>!wy(T,d.get(T.id))),draftsByWidgetId:Object.fromEntries(Object.entries(i.draftsByWidgetId).filter(([T,_])=>p.every(([C,g])=>C!==T||g!==_)))}}function wy(i,s){return s!==void 0&&i.id===s.id&&i.widgetId===s.widgetId&&i.widgetLabel===s.widgetLabel&&i.sourceLocation===s.sourceLocation&&i.visibleText===s.visibleText&&i.semanticInfo===s.semanticInfo&&i.text===s.text&&Yy(i.snapshot,s.snapshot)}function Yy(i,s){return i.status!==s.status?!1:i.status==="available"?s.status==="available"&&i.path===s.path&&i.mimeType===s.mimeType&&i.sizeBytes===s.sizeBytes:!0}function Gy(i,s,r={}){const f=new Map(s.map(d=>[d.id,d]));return i.map(d=>{const p=f.get(d.id);return{...d,snapshot:r[d.id]??p?.snapshot??d.snapshot}})}function Xy(i,s){return s===null?"":i.draftsByWidgetId[s.id]??""}function Zd(i,s,r){return s===null?i:{...i,draftsByWidgetId:{...i.draftsByWidgetId,[s.id]:r}}}function Qy(i,s,r){return{...i,comments:[...i.comments,{id:`selection-comment-${i.nextCommentId}`,widgetId:s.id,widgetLabel:s.displayLabel,...s.sourceLocation?{sourceLocation:s.sourceLocation}:{},...s.visibleText?{visibleText:s.visibleText}:{},...s.semanticInfo?{semanticInfo:s.semanticInfo}:{},text:r.trim(),snapshot:{status:"capturing"}}],nextCommentId:i.nextCommentId+1}}function Vy(i,s,r){const f=r.trim();return f.length===0?i:{...i,comments:i.comments.map(d=>d.id===s?{...d,text:f}:d)}}function ih(i,s,r){return i.comments.some(f=>f.id===s)?{...i,comments:i.comments.map(f=>f.id===s?{...f,snapshot:r}:f)}:i}function Zy(i,s){return{...i,comments:i.comments.filter(r=>r.id!==s)}}function Ky({emptyState:i,selectionComments:s,title:r}){const f=s.panelTarget,d=By();return S.jsxs("section",{className:"selected-widget-card","aria-labelledby":"selected-widget-title",children:[S.jsx("div",{id:"selected-widget-title",className:"chat-section-title",children:r}),f===null?S.jsx("p",{className:"chat-empty-state",children:i}):S.jsxs("div",{className:"selected-widget-content",children:[S.jsxs("div",{className:"selected-widget-summary",children:[S.jsx("div",{className:"selected-widget-name",children:f.displayLabel}),s.activeSelectionComment!==null?S.jsxs("div",{className:"selected-widget-meta",children:["From Attachment Token #",s.activeAttachmentNumber]}):null,f.sourceLocation?S.jsx("div",{className:"selected-widget-meta",children:f.sourceLocation}):null,f.visibleText?S.jsxs("div",{className:"selected-widget-detail",children:[S.jsx("span",{children:"Text:"}),S.jsx("strong",{children:f.visibleText})]}):null,f.semanticInfo?S.jsxs("div",{className:"selected-widget-detail",children:[S.jsx("span",{children:"Semantic"}),S.jsx("strong",{children:f.semanticInfo})]}):null]}),S.jsx("div",{className:"selection-comment-list","aria-label":"Selection Comments",children:s.selectedWidgetComments.length===0?S.jsx("div",{className:"selection-comment-empty",children:"No Selection Comments."}):s.selectedWidgetComments.map(p=>S.jsxs("div",{className:"selection-comment-item",children:[S.jsx("div",{className:"selection-comment-number",children:p.number}),S.jsx("textarea",{"aria-label":`Selection Comment ${p.number}`,className:"selection-comment-edit",disabled:s.isReadOnly,maxLength:d.maxLength,onChange:T=>{s.handleSelectionCommentTextChange(p.id,T.currentTarget.value)},rows:2,value:p.text}),S.jsx("button",{"aria-label":`Delete Selection Comment ${p.number}`,className:"selection-comment-delete",disabled:s.isReadOnly,onClick:()=>s.handleDeleteSelectionComment(p.id),type:"button",children:"Delete"})]},p.id))}),S.jsxs("div",{className:"selection-comment-composer",children:[S.jsx("textarea",{"aria-label":"Selection Comment",className:"selection-comment-input",disabled:f===null||s.isReadOnly,maxLength:d.maxLength,onChange:p=>{s.handleSelectionCommentDraftChange(p.currentTarget.value)},placeholder:"Comment on this widget...",ref:s.commentInputRef,rows:3,value:s.selectionCommentText}),S.jsxs("div",{className:"selection-comment-footer",children:[S.jsx("span",{className:"selection-comment-disabled-reason",children:s.selectionCommentInputState.disabledReason}),S.jsx("button",{className:"selection-comment-add",disabled:!s.selectionCommentInputState.canAdd,onClick:s.handleAddSelectionComment,type:"button",children:"Add comment"})]})]})]})]})}function Jy(){return{title:"Chat",agentStatusLabel:"Agent Status",agentStatusValue:"Waiting for agent",selectedWidgetTitle:"Selected widget",selectedWidgetEmptyState:"Select a widget to add Selection Comments.",chatHistoryTitle:"Chat History",chatHistoryEmptyState:"Chat History is empty.",composerPlaceholder:"Message the agent...",composerDisabledReason:"Agent Status is Waiting for agent."}}function Wy({composerText:i,selectionCommentCount:s,selectionCommentDraftsByWidgetId:r}){return i.trim().length>0||s>0||Object.values(r).some(f=>f.trim().length>0)}function $y({projectRoot:i,selectionComments:s,text:r}){if(s.length===0)return{text:r};const f=r.trim();return{context:{projectRoot:i},parts:[...s.map(d=>({type:"selection_comment",attachment:{id:d.id,commentText:d.text,selectedWidget:{id:d.widgetId,displayLabel:d.widgetLabel,...d.sourceLocation?{sourceLocation:ky(d.sourceLocation,i)}:{},...d.visibleText?{visibleText:d.visibleText}:{},...d.semanticInfo?{semanticInfo:d.semanticInfo}:{}},snapshot:d.snapshot.status==="available"?{status:"available",path:d.snapshot.path}:{status:"unavailable"}}})),...f.length>0?[{type:"text",text:f}]:[]]}}function ky(i,s){const r=i.trim(),f=s.trim().replace(/\/+$/,"");return f.length>0&&r.startsWith(`${f}/`)?r.slice(f.length+1):r}const ch={},Fy="http://127.0.0.1:8787";class sh extends Error{code;constructor(s,r){super(s),this.name="BridgeRequestError",this.code=r}}function Ju(i){const s=i?.trim().replace(/\/+$/,"");return s||Fy}async function El(i,s){const r=await i.text();if(!r.trim())throw new Error(`${s}: empty response`);try{return JSON.parse(r)}catch{throw new Error(`${s}: non-JSON response`)}}let me=Ju(ch?.VITE_ASK_UI_BRIDGE_ORIGIN);function fh(i){me=Ju(i)}function Ku(){me=Ju(ch?.VITE_ASK_UI_BRIDGE_ORIGIN)}function Vl(i,s){return new sh(i.message??i.error??s,i.error)}function rh(i){return i==="waiting_for_agent"||i==="agent_ready"||i==="agent_working"}function As(i,s,r={}){const f=r.createEventSource?.(`${me}/api/sessions/${encodeURIComponent(i)}/events`)??new EventSource(`${me}/api/sessions/${encodeURIComponent(i)}/events`);return f.addEventListener("bridge_session_event",d=>{try{s(Iy(d.data))}catch(p){r.onInvalidEvent?.(p instanceof Error?p:new Error("Invalid bridge event"))}}),f.addEventListener("error",()=>{r.onDisconnect?.()}),{close(){f.close()}}}function Iy(i){const s=JSON.parse(i);if(!s.type||!Py(s.type))throw new Error("Bridge session event has an unknown type");if(typeof s.sessionId!="string")throw new Error("Bridge session event did not include sessionId");if(!s.payload||typeof s.payload!="object")throw new Error("Bridge session event did not include payload");return tv(s),s}function Py(i){return i==="select_widget_mode_snapshot"||i==="select_widget_mode_changed"||i==="widget_selection_changed"||i==="chat_snapshot"||i==="agent_status_changed"||i==="chat_history_changed"}function tv(i){if(!(i.type==="select_widget_mode_snapshot"||i.type==="select_widget_mode_changed")){if(i.type==="widget_selection_changed"&&typeof i.payload.widgetId!="string")throw new Error("Widget Selection event did not include widgetId");if((i.type==="chat_snapshot"||i.type==="agent_status_changed")&&!rh(i.payload.agentStatus))throw new Error("Chat event did not include Agent Status");if((i.type==="chat_snapshot"||i.type==="chat_history_changed")&&!Array.isArray(i.payload.messages))throw new Error("Chat event did not include Chat History")}}async function ev(i,s=null){const r=new URL(`${me}/api/sessions/${encodeURIComponent(i)}/chat`);s&&r.searchParams.set("clientId",s);const f=await fetch(r.toString()),d=await El(f,"Failed to load Chat History");if(!f.ok)throw Vl(d,"Failed to load Chat History");if(d.status!=="ok"||!rh(d.agentStatus)||!Array.isArray(d.messages))throw new Error("Chat response did not include Chat History");return{status:"ok",agentStatus:d.agentStatus,readOnly:d.readOnly===!0,messages:d.messages}}async function lv(i,s){const r=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/chat/messages`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(s)}),f=await El(r,"Failed to send Chat message");if(!r.ok)throw Vl(f,"Failed to send Chat message");if(f.status!=="ok"||!f.message)throw new Error("Chat send response did not include the sent message");return{status:"ok",message:f.message}}async function av(i,s){const r=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/select-widget-mode`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({enabled:s})}),f=await El(r,"Failed to set Select Widget mode");if(!r.ok)throw Vl(f,"Failed to set Select Widget mode");if(f.status!=="ok"||typeof f.enabled!="boolean")throw new Error("Select Widget mode response did not include enabled state");return f}async function oh(i,s){const r=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/widget-selection`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({widgetId:s})}),f=await El(r,"Failed to select Flutter widget");if(!r.ok)throw Vl(f,"Failed to select Flutter widget");if(f.status!=="ok"||typeof f.widgetId!="string")throw new Error("Widget selection response did not include widgetId");return f}async function nv(i,s){const r=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/snapshots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({commentId:s,format:"png",maxSizeBytes:1258291,scope:"full_device"})}),f=await El(r,"Failed to capture Selection Comment snapshot");return!r.ok||f.status==="unavailable"?{status:"unavailable"}:f.status==="ok"&&f.snapshot?.mimeType==="image/png"&&typeof f.snapshot.path=="string"&&typeof f.snapshot.sizeBytes=="number"?{status:"available",path:f.snapshot.path,mimeType:f.snapshot.mimeType,sizeBytes:f.snapshot.sizeBytes}:{status:"unavailable"}}async function uv(i){const s=await fetch(`${me}/api/sessions`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(i)}),r=await El(s,"Failed to create Ask UI bridge session");if(!s.ok)throw Vl(r,"Failed to create Ask UI bridge session");if(!r.sessionId)throw new Error("Bridge session response did not include sessionId");const f={sessionId:r.sessionId,readOnly:r.readOnly===!0};return r.targetDevice&&typeof r.targetDevice=="object"&&"id"in r.targetDevice&&typeof r.targetDevice.id=="string"&&(f.targetDevice={id:r.targetDevice.id,displayName:"displayName"in r.targetDevice&&typeof r.targetDevice.displayName=="string"?r.targetDevice.displayName:void 0}),f}function iv(i,s,r){const f=new URL(Ju(s));return f.protocol=f.protocol==="https:"?"wss:":"ws:",f.pathname=`/api/sessions/${encodeURIComponent(i)}/device`,f.search="",r?.debugVideo&&f.searchParams.set("debugVideo",r.debugVideo),f.hash="",f.toString()}async function Kd(i){const s=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/widget-tree`),r=await El(s,"Failed to fetch Flutter Widget Tree");if(!s.ok)throw Vl(r,"Failed to fetch Flutter Widget Tree");if(!r.root)throw new Error("Widget Tree response did not include root");return{root:r.root}}async function cv(i){return dh(i,"hot-reload","Failed to hot reload Flutter app")}async function sv(i){return dh(i,"hot-restart","Failed to hot restart Flutter app")}async function dh(i,s,r){const f=await fetch(`${me}/api/sessions/${encodeURIComponent(i)}/${s}`,{method:"POST"}),d=await El(f,r);if(!f.ok)throw Vl(d,r);if(d.status!=="ok")throw new Error(`${r}: missing ok status`);return d}function fv({getSelectionCommentDraftsByWidgetIdForSend:i,getSelectionCommentsForSend:s,hasCapturingSnapshots:r,onComposerTextAfterSend:f,onSelectionCommentStateChange:d,projectRoot:p,sessionId:T,snapshotWaitMs:_,waitForPendingSnapshots:C}){const[g,D]=q.useState(!1),[b,H]=q.useState(!1),[O,B]=q.useState(null);async function L({composerInputRef:w,text:et}){if(T===null||p===null)return;D(!0),B(null);const lt=s(),bt=i();try{let F={};r()&&(H(!0),F=await C(_),H(!1));const ot=Gy(lt,s(),F);await lv(T,$y({projectRoot:p,selectionComments:ot,text:et})),f(!0,et),d(V=>Ly(V,!0,ot,bt)),requestAnimationFrame(()=>w.current?.focus())}catch(F){f(!1,et),B(F instanceof Error?F.message:"Failed to send Chat message.")}finally{H(!1),D(!1)}}function W(){B(null)}return{clearSendError:W,isFinishingSnapshots:b,isSending:g,send:L,sendError:O}}function rv({chatSessionState:i,defaultDisabledReason:s,getSelectionCommentDraftsByWidgetIdForSend:r,getSelectionCommentsForSend:f,hasCapturingSnapshots:d,onSelectionCommentStateChange:p,projectRoot:T,sessionId:_,snapshotWaitMs:C,waitForPendingSnapshots:g}){const[D,b]=q.useState(""),H=q.useRef(null),O=fv({getSelectionCommentDraftsByWidgetIdForSend:r,getSelectionCommentsForSend:f,hasCapturingSnapshots:d,onComposerTextAfterSend(F,ot){b(V=>yy(V,F,ot))},onSelectionCommentStateChange:p,projectRoot:T,sessionId:_,snapshotWaitMs:C,waitForPendingSnapshots:g}),B=hy(i,D,O.isSending,f().length),L=O.isFinishingSnapshots?"Finishing snapshots...":B.disabledReason??s,W=r(),w=Wy({composerText:D,selectionCommentCount:f().length,selectionCommentDraftsByWidgetId:W});q.useEffect(()=>{if(!w)return;function F(ot){ot.preventDefault(),ot.returnValue=""}return window.addEventListener("beforeunload",F),()=>{window.removeEventListener("beforeunload",F)}},[w]);async function et(F){F.preventDefault(),!(!B.canSend||_===null||T===null)&&await O.send({composerInputRef:H,text:D})}function lt(F){b(F),O.clearSendError()}function bt(F){gy(F.key,F.shiftKey)&&(F.preventDefault(),F.currentTarget.form?.requestSubmit())}return{composerDisabledReason:L,composerInputRef:H,composerState:B,composerText:D,handleComposerKeyDown:bt,handleComposerTextChange:lt,handleSubmit:et,sendError:O.sendError}}function ov({captureSnapshot:i,commentId:s,pendingSnapshots:r,sessionId:f,updateState:d}){const p=i(f,s).catch(()=>({status:"unavailable"})).then(T=>(d(_=>_.comments.find(g=>g.id===s)?.snapshot.status!=="capturing"?_:ih(_,s,T)),T)).finally(()=>{r.delete(s)});r.set(s,p)}async function dv({getState:i,pendingSnapshots:s,timeoutMs:r,updateState:f}){const d=i().comments.filter(g=>g.snapshot.status==="capturing").map(g=>({commentId:g.id,capture:s.get(g.id)})).filter(g=>g.capture!==void 0);if(d.length===0)return{};const p={};let T=!1;if(await Promise.race([Promise.allSettled(d.map(g=>g.capture.then(D=>{p[g.commentId]=D}))),new Promise(g=>{setTimeout(()=>{T=!0,g()},r)})]),!T)return p;const _=new Set(d.filter(g=>p[g.commentId]===void 0).map(g=>g.commentId)),C=Object.fromEntries([..._].map(g=>[g,{status:"unavailable"}]));return f(g=>({...g,comments:g.comments.map(D=>_.has(D.id)&&D.snapshot.status==="capturing"?{...D,snapshot:{status:"unavailable"}}:D)})),{...p,...C}}function hv({activeSelectionCommentId:i,attachmentTokens:s,isReadOnly:r,isSelectWidgetActive:f,onSelectionCommentStateChange:d,selectedWidget:p,selectionCommentState:T,sessionId:_,widgetTreeStatus:C}){const g=q.useRef(null),D=q.useRef(new Map),b=q.useRef(T);b.current=T;const H=i===null?null:Ry(T,i),O=q.useMemo(()=>Uy(p,H),[H,p]),B=H===null?null:s.find(Mt=>Mt.id===H.id)?.number??null,L=Xy(T,O),W=jy(T,O),w=Hy({isReadOnly:r,isSelectWidgetActive:f,selectedWidget:O,widgetTreeStatus:C,text:L,batchSize:T.comments.length}),et=q.useCallback(()=>{if(!w.canAdd||O===null)return;const Mt=`selection-comment-${T.nextCommentId}`;d(jt=>{const Yt=Qy(jt,O,L);return Zd(Yt,O,"")}),_===null?d(jt=>ih(jt,Mt,{status:"unavailable"})):ov({captureSnapshot:nv,commentId:Mt,pendingSnapshots:D.current,sessionId:_,updateState:d}),requestAnimationFrame(()=>g.current?.focus())},[d,O,w.canAdd,T.nextCommentId,L,_]),lt=q.useCallback((Mt,jt)=>{d(Yt=>Vy(Yt,Mt,jt))},[d]),bt=q.useCallback(Mt=>{d(jt=>Zy(jt,Mt))},[d]),F=q.useCallback(Mt=>{d(jt=>Zd(jt,p,Mt))},[d,p]),ot=q.useCallback(()=>b.current.comments.some(Mt=>Mt.snapshot.status==="capturing"),[]),V=q.useCallback(Mt=>dv({getState:()=>b.current,pendingSnapshots:D.current,timeoutMs:Mt,updateState:d}),[d]),Et=q.useCallback(()=>b.current.comments,[]),Nt=q.useCallback(()=>b.current.draftsByWidgetId,[]);return{activeAttachmentNumber:B,activeSelectionComment:H,commentInputRef:g,handleAddSelectionComment:et,handleDeleteSelectionComment:bt,handleSelectionCommentDraftChange:F,handleSelectionCommentTextChange:lt,hasCapturingSnapshots:ot,getSelectionCommentDraftsByWidgetIdForSend:Nt,getSelectionCommentsForSend:Et,panelTarget:O,isReadOnly:r,selectedWidgetComments:W,selectionCommentInputState:w,selectionCommentText:L,waitForPendingSnapshots:V}}const mv=5e3;function gv({activeSelectionCommentId:i,attachmentTokens:s,chatSessionState:r,isReadOnly:f,isSelectWidgetActive:d,onAttachmentTokenClick:p,onSelectionCommentStateChange:T,projectRoot:_,selectedWidget:C,selectionCommentState:g,sessionId:D,widgetTreeStatus:b}){const H=Jy(),O=hv({activeSelectionCommentId:i,attachmentTokens:s,isReadOnly:f,isSelectWidgetActive:d,onSelectionCommentStateChange:T,selectedWidget:C,selectionCommentState:g,sessionId:D,widgetTreeStatus:b}),B=rv({chatSessionState:r,defaultDisabledReason:H.composerDisabledReason,getSelectionCommentDraftsByWidgetIdForSend:O.getSelectionCommentDraftsByWidgetIdForSend,getSelectionCommentsForSend:O.getSelectionCommentsForSend,hasCapturingSnapshots:O.hasCapturingSnapshots,onSelectionCommentStateChange:T,projectRoot:_,sessionId:D,snapshotWaitMs:mv,waitForPendingSnapshots:O.waitForPendingSnapshots}),L=r.status==="ready"?lh(r.agentStatus):H.agentStatusValue,W=ry(r);return S.jsxs("aside",{className:"workbench-panel chat-panel","aria-label":H.title,children:[S.jsx(zy,{agentStatusLabel:H.agentStatusLabel,agentStatusValue:L,title:H.title}),S.jsx(Ky,{emptyState:H.selectedWidgetEmptyState,selectionComments:O,title:H.selectedWidgetTitle}),S.jsx(Ay,{chatSessionState:r,emptyState:H.chatHistoryEmptyState,title:H.chatHistoryTitle,visibleMessages:W}),S.jsx(vy,{attachmentTokens:s,chatSessionState:r,composer:B,onAttachmentTokenClick:p,placeholder:H.composerPlaceholder,sessionId:D})]})}function Jd(i,s){return s.status==="running"?`${i}...`:i}function yv({isSelectWidgetActive:i,hotReload:s,hotRestart:r,targetDeviceDisplay:f,statusMessage:d,onToggleSelectWidget:p,onHotReload:T,onHotRestart:_}){return S.jsxs("header",{className:"top-bar",children:[S.jsxs("div",{className:"top-bar-brand",children:[S.jsx("div",{className:"brand-mark","aria-hidden":"true"}),S.jsx("span",{children:"Ask UI"})]}),S.jsxs("div",{className:"top-bar-session","aria-label":"Target device status",children:[S.jsx("span",{className:"status-dot","aria-hidden":"true"}),S.jsx("span",{className:"target-device-label",title:f.title,children:f.topBarLabel}),S.jsx("span",{className:"session-muted",title:d,children:d})]}),S.jsxs("div",{className:"top-bar-actions","aria-label":"Workbench actions",children:[S.jsx("button",{"aria-pressed":i,className:`toolbar-button ${i?"toolbar-button-active":""}`,onClick:p,type:"button",children:"Select Widget"}),S.jsx("button",{className:`toolbar-button ${s.status==="failed"?"toolbar-button-error":""}`,disabled:s.status==="running",onClick:T,title:s.message,type:"button",children:Jd("Hot Reload",s)}),S.jsx("button",{className:"toolbar-button",disabled:r.status==="running",onClick:_,title:r.message,type:"button",children:Jd("Hot Restart",r)})]})]})}const vv={isSelectWidgetActive:!1,selectWidget:{status:"idle"},hotReload:{status:"idle"},hotRestart:{status:"idle"}};function Sv(i){const s=!i.isSelectWidgetActive;return{...i,isSelectWidgetActive:s,selectWidget:{status:"idle",message:s?"Select Widget mode enabled.":"Select Widget mode disabled."}}}function Wd(i,s){return{...i,[s]:{status:"failed",message:s==="hotReload"?"Bridge session required before hot reload.":"Bridge session required before hot restart."}}}function pv(i,s){return i.selectWidget.status==="running"?"Select Widget mode updating":i.hotReload.status==="running"?"Hot reload running":i.hotRestart.status==="running"?"Hot restart running":i.hotReload.message?i.hotReload.message:i.hotRestart.message?i.hotRestart.message:i.selectWidget.message?i.selectWidget.message:i.isSelectWidgetActive?"Select Widget mode on":s?.status==="incomplete"?"Session incomplete":s?.status==="creating"?"Connecting":s?.status==="error"?"Session error":"Ready"}function bv(i){const s=[];return mh(i,r=>{r.children.length>0&&s.push(r.id)}),s}function Tv({root:i,expandedNodeIds:s}){const r=[];return hh({node:i,depth:0,expandedNodeIds:s,rows:r}),r}function hh({node:i,depth:s,expandedNodeIds:r,rows:f}){const d=i.children.length>0;if(f.push({id:i.id,depth:s,node:i,hasChildren:d}),!d||!r.has(i.id))return;const p=i.children.length===1?s:s+1;for(const T of i.children)hh({node:T,depth:p,expandedNodeIds:r,rows:f})}function mh(i,s){s(i);for(const r of i.children)mh(r,s)}function $d(i,s){const r=s.trim().toLowerCase();if(!r)return[];const f=[];return Es(i,[],(d,p)=>{d.label.toLowerCase().includes(r)&&f.push({nodeId:d.id,label:d.label,ancestorNodeIds:p})}),f}function Av({currentIndex:i,total:s}){return s<=0?-1:(i+1)%s}function Ev({currentIndex:i,total:s}){return s<=0?-1:i<0?s-1:(i-1+s)%s}function zv(i){const s=[],r=new Set;for(const f of i)for(const d of f.ancestorNodeIds)r.has(d)||(r.add(d),s.push(d));return s}function _v(i,s){if(s===null)return[];let r=[];return Es(i,[],(f,d)=>{f.id===s&&(r=d)}),r}function Es(i,s,r){r(i,s);for(const f of i.children)Es(f,[...s,i.id],r)}function Nv(i){return Cv(i)?{kind:"custom",glyph:"◆",title:"Project widget"}:Oa(i,["MaterialApp","Scaffold","Theme","[root]"])?{kind:"app",glyph:"□",title:"App structure widget"}:Oa(i,["Text","RichText","DefaultTextStyle","EditableText"])?{kind:"text",glyph:"T",title:"Text widget"}:Oa(i,["Gesture","Listener","TrackpadListener","Tap","Pointer"])?{kind:"gesture",glyph:"⌁",title:"Interaction widget"}:Oa(i,["Image","Opacity","Transform","Clip","DecoratedBox","FractionalTranslation"])?{kind:"visual",glyph:"▧",title:"Visual widget"}:Oa(i,["Animated","Animation","Animate","ValueListenableBuilder"])?{kind:"animation",glyph:"◇",title:"Animation widget"}:Oa(i,["Container","Stack","Column","Row","Padding","SizedBox","Center","Positioned","LayoutBuilder","KeyedSubtree","ScrollConfiguration"])?{kind:"layout",glyph:"▣",title:"Layout widget"}:{kind:"widget",glyph:"·",title:"Widget"}}function Cv(i){return/^[A-Z]/.test(i)&&/^Wonder|Wonders|Home|Previous|Bottom/.test(i)}function Oa(i,s){return s.some(r=>i.includes(r))}function xv({root:i,selectedWidgetId:s,searchActiveWidgetId:r,searchExpandedAncestorIds:f,onSelectWidget:d}){const p=q.useMemo(()=>bv(i),[i]),T=q.useRef(null),[_,C]=q.useState(()=>new Set(p)),g=q.useMemo(()=>Tv({root:i,expandedNodeIds:_}),[_,i]);q.useEffect(()=>{C(new Set(p))},[p]),q.useEffect(()=>{f.length!==0&&C(b=>{const H=new Set(b);for(const O of f)H.add(O);return H})},[f]),q.useEffect(()=>{const b=_v(i,s);b.length!==0&&C(H=>{const O=new Set(H);for(const B of b)O.add(B);return O})},[i,s]),q.useEffect(()=>{T.current?.scrollIntoView({block:"nearest"})},[s,g]);function D(b){C(H=>{const O=new Set(H);return O.has(b)?O.delete(b):O.add(b),O})}return S.jsx("div",{className:"widget-tree-flat-list",role:"tree",children:g.map(b=>{const H=b.hasChildren&&_.has(b.node.id),O=b.node.id===s,B=Nv(b.node.label);return S.jsxs("div",{"aria-expanded":b.hasChildren?H:void 0,"aria-level":b.depth+1,"aria-selected":O,className:`widget-tree-row ${O?"widget-tree-row-selected":""} ${b.node.id===r?"widget-tree-row-search-active":""}`,onClick:()=>d(b.node.id),onKeyDown:L=>{(L.key==="Enter"||L.key===" ")&&(L.preventDefault(),d(b.node.id))},role:"treeitem",style:{"--tree-depth":b.depth},tabIndex:0,ref:O?T:null,children:[S.jsx("button",{"aria-label":b.hasChildren?`${H?"Collapse":"Expand"} ${b.node.label}`:void 0,className:"widget-tree-toggle",disabled:!b.hasChildren,onClick:L=>{L.stopPropagation(),b.hasChildren&&D(b.node.id)},onPointerDown:L=>L.stopPropagation(),type:"button",children:b.hasChildren?H?"▾":"▸":""}),S.jsx("span",{className:"widget-tree-branch","aria-hidden":"true"}),S.jsx("span",{"aria-label":B.title,className:`widget-tree-type-icon widget-tree-type-icon-${B.kind}`,title:B.title,children:B.glyph}),S.jsx("span",{className:"widget-tree-label",title:b.node.label,children:b.node.label})]},b.id)})})}function Ov({bridgeSessionState:i,widgetTreeState:s,selectedWidgetId:r,searchActiveWidgetId:f,searchExpandedAncestorIds:d,onSelectWidget:p}){return i.status==="incomplete"?S.jsxs("div",{className:"widget-tree-state",children:[S.jsx("div",{className:"widget-tree-state-title",children:"Session parameters required"}),S.jsxs("div",{className:"widget-tree-state-copy",children:["Add ",i.missing.join(" and ")," to the page URL to create an Ask UI bridge session."]})]}):i.status==="creating"?S.jsxs("div",{className:"widget-tree-state",children:[S.jsx("div",{className:"widget-tree-state-title",children:"Creating bridge session"}),S.jsx("div",{className:"widget-tree-state-copy",children:"Ask UI is sending the Flutter VM Service URI and project root to the local bridge."})]}):i.status==="error"?S.jsxs("div",{className:"widget-tree-state widget-tree-state-error",children:[S.jsx("div",{className:"widget-tree-state-title",children:"Bridge session failed"}),S.jsx("div",{className:"widget-tree-state-copy",children:i.message})]}):s.status==="loading"?S.jsxs("div",{className:"widget-tree-state",children:[S.jsx("div",{className:"widget-tree-state-title",children:"Fetching Widget Tree"}),S.jsxs("div",{className:"widget-tree-state-copy",children:["Session ",i.sessionId," is reading the Flutter Inspector summary tree."]})]}):s.status==="error"?S.jsxs("div",{className:"widget-tree-state widget-tree-state-error",children:[S.jsx("div",{className:"widget-tree-state-title",children:"Widget Tree failed"}),S.jsx("div",{className:"widget-tree-state-copy",children:s.message})]}):S.jsx("div",{className:"widget-tree-root-list",children:S.jsx(xv,{root:s.root,selectedWidgetId:r,searchActiveWidgetId:f,searchExpandedAncestorIds:d,onSelectWidget:p})})}function Mv({bridgeSessionState:i,onRefresh:s,onSelectedWidgetIdChange:r,selectedWidgetId:f,widgetTreeState:d}){const[p,T]=q.useState(null),[_,C]=q.useState(""),[g,D]=q.useState(-1),b=q.useMemo(()=>d.status!=="loaded"?[]:$d(d.root,_),[_,d]),H=g>=0?b[g]?.nodeId??null:null,O=q.useMemo(()=>zv(b),[b]),B=_.trim().length>0,L=B&&b.length>0?`${g+1}/${b.length}`:B?"0/0":"",W=i.status==="ready"&&d.status!=="loading";q.useEffect(()=>{r(null),T(null),D(-1)},[r,d]);async function w(V){if(i.status!=="ready")return;const Et=f;r(V),T(null);try{await oh(i.sessionId,V)}catch(Nt){r(Et),T(Nt instanceof Error?Nt.message:"Failed to select Flutter widget")}}function et(V){const Et=b[V];if(!Et){D(-1);return}D(V),w(Et.nodeId)}function lt(V){if(C(V),!V.trim()||d.status!=="loaded"){D(-1);return}const Et=$d(d.root,V);if(Et.length===0){D(-1);return}D(0),w(Et[0].nodeId)}function bt(){et(Av({currentIndex:g,total:b.length}))}function F(){et(Ev({currentIndex:g,total:b.length}))}function ot(){s().catch(()=>{})}return S.jsxs("aside",{className:"workbench-panel widget-tree-panel",children:[S.jsxs("div",{className:"widget-tree-header",children:[S.jsxs("div",{children:[S.jsx("div",{className:"widget-tree-title",children:"Widget Tree"}),S.jsx("div",{className:"widget-tree-subtitle",children:"Flutter Inspector summary"})]}),S.jsx("button",{"aria-label":"Refresh widget tree",className:"widget-tree-icon-button",disabled:!W,onClick:ot,title:"Refresh widget tree",type:"button",children:"↻"})]}),S.jsxs("div",{className:"widget-tree-search",children:[S.jsx("span",{"aria-hidden":"true",className:"widget-tree-search-icon",children:"⌕"}),S.jsx("input",{"aria-label":"Search widget tree",className:"widget-tree-search-input",onChange:V=>lt(V.currentTarget.value),onKeyDown:V=>{V.key==="Enter"&&(V.preventDefault(),bt())},placeholder:"Search widgets",value:_,type:"search"}),L?S.jsx("span",{className:"widget-tree-search-counter",children:L}):null,B?S.jsxs("div",{className:"widget-tree-search-actions",children:[S.jsx("button",{"aria-label":"Previous widget tree search match",className:"widget-tree-search-action widget-tree-search-action-previous",disabled:b.length===0,onClick:F,title:"Previous match",type:"button"}),S.jsx("button",{"aria-label":"Next widget tree search match",className:"widget-tree-search-action widget-tree-search-action-next",disabled:b.length===0,onClick:bt,title:"Next match",type:"button"})]}):null]}),p?S.jsx("div",{className:"widget-tree-selection-error",children:p}):null,S.jsx(Ov,{bridgeSessionState:i,widgetTreeState:d,selectedWidgetId:f,searchActiveWidgetId:H,searchExpandedAncestorIds:O,onSelectWidget:w})]})}function Rv({clientId:i,sessionId:s}){const[r,f]=q.useState({status:"idle"});return q.useEffect(()=>{if(s===null){f({status:"idle"});return}let d=!0,p=!1,T=null;const _=[];f({status:"loading"}),ev(s,i).then(g=>{d&&(T=sy(g,_),p=!0,f(T))},g=>{d&&(p=!0,f({status:"error",message:g instanceof Error?g.message:"Failed to load Chat History"}))});const C=As(s,g=>{if(!(!d||g.sessionId!==s)){if(!p){_.push(g);return}f(D=>{const b=D.status==="ready"?D:T;if(b===null)return D;const H=eh(b,g);return T=H,H})}},{onDisconnect(){d&&f(fy)}});return()=>{d=!1,C.close()}},[i,s]),r}function Uv(i){if(i.status==="incomplete")return{topBarLabel:"Device required",surfaceLabel:"Device required",title:"Device required",status:"incomplete"};if(i.status==="creating")return{topBarLabel:"Connecting device",surfaceLabel:"Connecting device",title:"Connecting device",status:"creating"};if(i.status==="error")return{topBarLabel:"Device unavailable",surfaceLabel:"Device unavailable",title:i.message,status:"error"};const s=i.targetDeviceDisplayName?.trim()??"",r=i.targetDeviceId?.trim()??"";return r?s?{topBarLabel:s,surfaceLabel:r,title:`${s} (${r})`,status:"ready"}:{topBarLabel:`Device ${r}`,surfaceLabel:r,title:r,status:"ready"}:{topBarLabel:"Attached session",surfaceLabel:"Attached session",title:"Attached bridge session",status:"ready"}}function Dv(i){return i===null?{status:"idle"}:{status:"connecting"}}function jv(i,s){const r=JSON.parse(s);return r.type==="ready"?{status:"waitingForVideo",metadata:kd(r)}:r.type==="metadata"&&"metadata"in i?{...i,metadata:kd(r)}:r.type==="error"?{status:"failed",error:typeof r.error=="string"?r.error:void 0,message:typeof r.message=="string"?r.message:"Device failed."}:i}function Hv(i){return i.status!=="waitingForVideo"?i:{status:"renderingVideo",metadata:i.metadata}}function kd(i){return{deviceId:String(i.deviceId),screenWidth:Number(i.screenWidth),screenHeight:Number(i.screenHeight),maxFps:Number(i.maxFps),videoCodec:"h264",controlReady:i.controlReady===!0}}class Bv extends Error{constructor(s){super(s),this.name="H264AnnexBParseError"}}class qv{bufferedBytes=new Uint8Array;accessUnitNalUnits=[];accessUnitHasVcl=!1;accessUnitCodec;latestParameterSets=[];latestCodec;push(s){this.bufferedBytes=Yv(this.bufferedBytes,s);const{nalUnits:r,remainingBytes:f}=Id(this.bufferedBytes,{flush:!1});if(this.bufferedBytes=f,r.length===0){if(gh(this.bufferedBytes).length>0)return[];if(this.bufferedBytes.length>=4&&!wv(this.bufferedBytes))throw new Bv("H.264 Annex B stream is missing a start code.");return[]}return this.pushNalUnits(r)}flush(){const{nalUnits:s,remainingBytes:r}=Id(this.bufferedBytes,{flush:!0});this.bufferedBytes=r;const f=this.pushNalUnits(s);return this.accessUnitHasVcl&&f.push(this.emitAccessUnit()),f}pushNalUnits(s){const r=[];for(const f of s)(f.type===7||f.type===8)&&(this.latestParameterSets=this.latestParameterSets.filter(d=>d.type!==f.type),this.latestParameterSets.push(f)),f.type===7&&(this.latestCodec=Lv(f.payload)),f.type===9&&this.accessUnitHasVcl&&r.push(this.emitAccessUnit()),Fd(f.type)&&this.accessUnitHasVcl&&Gv(f)&&r.push(this.emitAccessUnit()),f.type===5&&!this.accessUnitHasVcl&&!this.accessUnitNalUnits.some(d=>d.type===7)&&this.latestParameterSets.length>0&&this.accessUnitNalUnits.push(...this.latestParameterSets),this.accessUnitNalUnits.push(f),this.accessUnitCodec=this.accessUnitCodec??this.latestCodec,this.accessUnitHasVcl=this.accessUnitHasVcl||Fd(f.type);return r}emitAccessUnit(){const s=Vv(this.accessUnitNalUnits.map(d=>d.bytes)),r=this.accessUnitCodec,f=this.accessUnitNalUnits.map(d=>d.type);return this.accessUnitNalUnits=[],this.accessUnitHasVcl=!1,this.accessUnitCodec=void 0,{bytes:s,codec:r,nalTypes:f}}}function Lv(i){if(!(i.length<4))return`avc1.${Ss(i[1])}${Ss(i[2])}${Ss(i[3])}`}function Ss(i){return i.toString(16).padStart(2,"0").toUpperCase()}function wv(i){return i.every((s,r)=>r=1&&i<=5}function Yv(i,s){const r=new Uint8Array(i.length+s.length);return r.set(i,0),r.set(s,i.length),r}function Id(i,{flush:s}){const r=gh(i),f=[];if(r.length<2){if(s&&r.length===1){const p=r[0],T=p.index+p.length;if(T=2&&i[r-2]===0&&i[r-1]===0&&i[r]===3||s.push(i[r]);return new Uint8Array(s)}function Qv(i){let s=0,r=0;for(;r=i.length*8)return-1;r+=1;let f=0;for(let d=0;d>f&1}function Vv(i){const s=i.reduce((d,p)=>d+p.length,0),r=new Uint8Array(s);let f=0;for(const d of i)r.set(d,f),f+=d.length;return r}const Zv=1;function Kv({webCodecs:i,onError:s,onFrame:r,onFirstFrameRendered:f}){const d=i.VideoDecoder,p=i.EncodedVideoChunk;if(!d||!p)return{status:{type:"unsupported",message:"WebCodecs is not available in this browser."},close(){},flush(){},push(){}};const T=new qv;let _=0,C=null,g="ready";const D=O=>{g==="ready"&&(g="failed",console.error("Device video decoder failed",O),s?.(O))},b=new d({output(O){if(g!=="ready"){O.close?.();return}r(O)},error(O){D(O)}}),H=O=>{for(const B of O){if(g!=="ready")return;if(B.codec&&B.codec!==C&&(b.configure({codec:B.codec,optimizeForLatency:!0}),C=B.codec),!C)continue;const L=B.nalTypes.includes(5)?"key":"delta";if(L==="delta"&&b.decodeQueueSize>Zv)continue;const W=new p({type:L,timestamp:_,data:B.bytes});_+=1;try{b.decode(W)}catch(w){D(w instanceof Error?w:new Error(String(w)));return}}};return{status:{type:"ready"},close(){g!=="closed"&&(g="closed",b.close())},flush(){if(g==="ready")try{H(T.flush())}catch(O){D(O instanceof Error?O:new Error(String(O)))}},push(O){let B;try{B=T.push(O)}catch(L){D(L instanceof Error?L:new Error(String(L)));return}H(B)}}}function Jv({onFrameRendered:i}){let s=null,r=null;const f=(d,p)=>{d.render(p),i()};return{setRenderer(d){if(s=d,!s||!r)return;const p=r;r=null,f(s,p)},render(d){const p=d;if(!s){r?.close?.(),r=p;return}f(s,p)},close(){r?.close?.(),r=null}}}function Wv(i){const[s,r]=q.useState(0),f=q.useRef(null),d=q.useRef(null),[p,T]=q.useState(()=>Dv(i));d.current===null&&(d.current=Jv({onFrameRendered(){T(g=>Hv(g))}}));const _=q.useCallback(g=>{const D=f.current;D?.readyState===WebSocket.OPEN&&D.send(JSON.stringify(g))},[]),C=q.useCallback(g=>{d.current?.setRenderer(g)},[]);return q.useEffect(()=>{if(i===null){T({status:"idle"});return}let g=!1,D=!1,b=!1;const H=kv(),O=Kv({webCodecs:$v(),onError:W=>{D=!0,console.error("Device video decode failed",W),T({status:"failed",error:"video_decode_failed",message:"Device video failed to decode."})},onFrame:W=>{d.current?.render(W)},onFirstFrameRendered:()=>{}});if(O.status.type==="unsupported")return T({status:"failed",error:"webcodecs_unavailable",message:O.status.message}),()=>O.close();const B=Fv(O),L=new WebSocket(iv(i,me,H));return L.binaryType="arraybuffer",f.current=L,T({status:"connecting"}),L.addEventListener("message",W=>{if(b)return;if(W.data instanceof ArrayBuffer){B.push(new Uint8Array(W.data)),H.debugVideo==="fixture"&&B.flush();return}if(W.data instanceof Blob){W.data.arrayBuffer().then(et=>{b||(B.push(new Uint8Array(et)),H.debugVideo==="fixture"&&B.flush())});return}const w=typeof W.data=="string"?W.data:String(W.data);D=w.includes('"type":"error"'),T(et=>jv(et,w))}),L.addEventListener("error",()=>{D=!0,T({status:"failed",error:"device_start_failed",message:"Device failed to start."})}),L.addEventListener("close",()=>{B.flush(),!(g||D)&&T({status:"failed",error:"device_failed",message:"Device disconnected."})}),()=>{g=!0,b=!0,f.current===L&&(f.current=null),d.current?.close(),B.close(),L.close()}},[s,i]),{surfaceState:p,retryLiveAppSurface(){r(g=>g+1)},sendDeviceControlMessage:_,setDeviceVideoRenderer:C}}function $v(){return typeof window>"u"?{}:{EncodedVideoChunk:window.EncodedVideoChunk,VideoDecoder:window.VideoDecoder}}function kv(){if(typeof window>"u")return{};const i=new URLSearchParams(window.location.search).get("debugVideo");return i==="fixture"?{debugVideo:i}:{}}function Fv(i){if(i.status.type!=="ready")throw new Error("Device video pipeline is not ready.");return i}function yh(i){const s=new URL(i).searchParams,r=s.get("bridgeUrl")?.trim()??"",f=s.get("sessionId")?.trim()??"";if(r||f){const C=[];if(r||C.push("bridgeUrl"),f||C.push("sessionId"),C.length>0)return{mode:"attach",status:"incomplete",missing:C};const g=s.get("deviceId")?.trim()||void 0,D=s.get("projectRoot")?.trim()||void 0;return{mode:"attach",status:"ready",bridgeUrl:r.replace(/\/+$/,""),sessionId:f,...g?{deviceId:g}:{},...D?{projectRoot:D}:{}}}const d=s.get("vmServiceUri")?.trim()??"",p=s.get("projectRoot")?.trim()??"",T=s.get("deviceId")?.trim()??"",_=[];return d||_.push("vmServiceUri"),p||_.push("projectRoot"),T||_.push("deviceId"),_.length>0?{mode:"create",status:"incomplete",missing:_}:{mode:"create",status:"ready",vmServiceUri:d,projectRoot:p,deviceId:T}}function Iv(i){const s=yh(i);return s.status==="incomplete"?(s.mode==="create"&&Ku(),{status:"incomplete",missing:s.missing}):s.mode==="attach"?(fh(s.bridgeUrl),{status:"ready",sessionId:s.sessionId,projectRoot:s.projectRoot??"",targetDeviceId:s.deviceId,clientId:bs(),readOnly:!1}):(Ku(),{status:"creating"})}function bs(){const i="ask-ui-bridge-client-id";try{const s=window.sessionStorage.getItem(i);if(s)return s;const r=window.crypto.randomUUID();return window.sessionStorage.setItem(i,r),r}catch{return`client-${Math.random().toString(36).slice(2)}`}}function Pv(i){const[s,r]=q.useState(()=>Iv(i));return q.useEffect(()=>{const f=yh(i);let d=!0;if(f.status==="incomplete"){f.mode==="create"&&Ku(),r({status:"incomplete",missing:f.missing});return}if(f.mode==="attach"){fh(f.bridgeUrl),r({status:"ready",sessionId:f.sessionId,projectRoot:f.projectRoot??"",targetDeviceId:f.deviceId,clientId:bs(),readOnly:!1});return}Ku(),r({status:"creating"});const p=bs();return uv({vmServiceUri:f.vmServiceUri,projectRoot:f.projectRoot,deviceId:f.deviceId,clientId:p}).then(({sessionId:T,targetDevice:_,readOnly:C})=>{d&&r({status:"ready",sessionId:T,projectRoot:f.projectRoot,targetDeviceId:f.deviceId,targetDeviceDisplayName:_?.displayName?.trim()||void 0,clientId:p,readOnly:C})},T=>{d&&r({status:"error",message:T instanceof Error?T.message:"Failed to create Ask UI bridge session"})}),()=>{d=!1}},[i]),{bridgeSessionState:s,readySessionId:s.status==="ready"?s.sessionId:null}}function th(i){return i instanceof Error?i.message:"Failed to fetch Flutter Widget Tree"}function t0(i){const[s,r]=q.useState({status:"loading"}),f=q.useCallback(async()=>{if(i!==null){r({status:"loading"});try{const{root:d}=await Kd(i);r({status:"loaded",root:d})}catch(d){throw r({status:"error",message:th(d)}),d}}},[i]);return q.useEffect(()=>{if(i===null){r({status:"loading"});return}let d=!0;return r({status:"loading"}),Kd(i).then(({root:p})=>{d&&r({status:"loaded",root:p})},p=>{d&&r({status:"error",message:th(p)})}),()=>{d=!1}},[i]),{widgetTreeState:s,setWidgetTreeState:r,refreshWidgetTree:f}}function e0(i){const{sessionId:s,isReadOnly:r,widgetTreeState:f,setWidgetTreeState:d,refreshWidgetTree:p}=i,[T,_]=q.useState(vv),C=q.useRef({didMount:!1,previousActive:T.isSelectWidgetActive,skipNextSync:!1}),g=q.useCallback(O=>{if(O.sessionId!==s||O.type!=="select_widget_mode_snapshot"&&O.type!=="select_widget_mode_changed"||typeof O.payload.enabled!="boolean")return;const B=O.payload.enabled;_(L=>L.isSelectWidgetActive===B?L:(C.current.skipNextSync=!0,{...L,isSelectWidgetActive:B,selectWidget:{status:"idle",message:B?"Select Widget mode enabled.":"Select Widget mode disabled."}}))},[s]);q.useEffect(()=>{r&&(C.current.skipNextSync=!0,_(O=>({...O,isSelectWidgetActive:!1,selectWidget:{status:"idle",message:"Read-only browser tabs cannot use Select Widget mode."}})))},[r]),q.useEffect(()=>{if(!C.current.didMount){C.current.didMount=!0,C.current.previousActive=T.isSelectWidgetActive;return}if(C.current.skipNextSync){C.current.skipNextSync=!1,C.current.previousActive=T.isSelectWidgetActive;return}if(C.current.previousActive===T.isSelectWidgetActive||(C.current.previousActive=T.isSelectWidgetActive,s===null||r))return;let O=!0;const B=T.isSelectWidgetActive;return _(L=>({...L,selectWidget:{status:"running"}})),av(s,B).then(()=>{O&&_(L=>({...L,selectWidget:{status:"idle",message:B?"Select Widget mode enabled.":"Select Widget mode disabled."}}))},L=>{O&&(C.current.skipNextSync=!0,_(W=>({...W,isSelectWidgetActive:!B,selectWidget:{status:"failed",message:L instanceof Error?L.message:"Failed to set Select Widget mode"}})))}),()=>{O=!1}},[r,s,T.isSelectWidgetActive]),q.useEffect(()=>{if(s===null)return;const O=As(s,g);return()=>{O.close()}},[s,g]);function D(){if(s===null){_(O=>({...O,selectWidget:{status:"failed",message:"Bridge session required before Select Widget mode."}}));return}if(r){_(O=>({...O,selectWidget:{status:"failed",message:"Read-only browser tabs cannot use Select Widget mode."}}));return}_(Sv)}async function b(){if(s===null){_(B=>Wd(B,"hotReload"));return}const O=f;_(B=>({...B,hotReload:{status:"running"}})),d({status:"loading"});try{await cv(s)}catch(B){_(L=>({...L,hotReload:{status:"failed",message:B instanceof Error?B.message:"Hot reload failed"}})),d(O);return}try{await p(),_(B=>({...B,hotReload:{status:"idle",message:"Hot reload completed."}}))}catch(B){d({status:"error",message:B instanceof Error?B.message:"Failed to fetch Flutter Widget Tree"}),_(L=>({...L,hotReload:{status:"idle",message:"Hot reload completed, but Widget Tree refresh failed."}}))}}async function H(){if(s===null){_(O=>Wd(O,"hotRestart"));return}_(O=>({...O,hotRestart:{status:"running"}}));try{await sv(s),_(O=>({...O,hotRestart:{status:"idle",message:"Hot restart completed."}}))}catch(O){const B=O instanceof sh&&O.code==="hot_restart_not_supported_for_session";_(L=>({...L,hotRestart:{status:B?"unsupported":"failed",message:O instanceof Error?O.message:"Hot restart failed"}}))}}return{topBarActionState:T,handleToggleSelectWidget:D,handleHotReload:b,handleHotRestart:H}}function l0(i,s,r){return Math.min(r,Math.max(s,i))}function a0(i){const{minWidth:s,maxWidth:r,defaultWidth:f}=i,[d,p]=q.useState(f),T=q.useRef(null);function _(H){p(l0(H,s,r))}function C(H){H.currentTarget.setPointerCapture(H.pointerId),T.current={pointerId:H.pointerId,startX:H.clientX,startWidth:d}}function g(H){const O=T.current;!O||O.pointerId!==H.pointerId||_(O.startWidth+H.clientX-O.startX)}function D(H){T.current?.pointerId===H.pointerId&&(T.current=null,H.currentTarget.releasePointerCapture(H.pointerId))}function b(H){H.key==="ArrowLeft"&&(H.preventDefault(),_(d-16)),H.key==="ArrowRight"&&(H.preventDefault(),_(d+16)),H.key==="Home"&&(H.preventDefault(),p(s)),H.key==="End"&&(H.preventDefault(),p(r))}return{contentStyle:{"--widget-tree-width":`${d}px`},resizeHandleProps:{"aria-label":"Resize widget tree panel","aria-orientation":"vertical","aria-valuemax":r,"aria-valuemin":s,"aria-valuenow":d,className:"panel-resize-handle",onKeyDown:b,onPointerDown:C,onPointerMove:g,onPointerUp:D,role:"separator",tabIndex:0}}}function n0({isSelectWidgetActive:i,sessionId:s,widgetTreeState:r}){const[f,d]=q.useState(null),[p,T]=q.useState(null),[_,C]=q.useState(qy),g=q.useMemo(()=>r.status!=="loaded"?null:_y(r.root,f),[f,r]),D=q.useMemo(()=>r.status!=="loaded"?new Set:Ny(r.root),[r]),b=q.useMemo(()=>r.status!=="loaded"?new Map:Cy(r.root),[r]),H=q.useMemo(()=>Oy(_,D),[D,_]),O=q.useMemo(()=>My({isSelectWidgetActive:i,locatableWidgetBoundsById:b,state:_}),[i,b,_]),B=q.useCallback(W=>{d(W),T(null)},[]);q.useEffect(()=>{if(s===null)return;const W=As(s,w=>{w.sessionId!==s||w.type!=="widget_selection_changed"||B(w.payload.widgetId)});return()=>{W.close()}},[B,s]);const L=q.useCallback(W=>{T(W.id),!(!W.isLocatable||s===null)&&(d(W.widgetId),oh(s,W.widgetId).catch(()=>{}))},[s]);return{activeSelectionCommentId:p,attachmentTokens:H,handleAttachmentTokenClick:L,handleSelectedWidgetIdChange:B,selectedWidget:g,selectedWidgetId:f,selectionCommentState:_,setSelectionCommentState:C,overlayMarkers:O}}function u0(i,s){return s.status==="ready"?s.readOnly:i.status==="ready"?i.readOnly:!1}function i0(){const{bridgeSessionState:i,readySessionId:s}=Pv(window.location.href),r=Wv(s),f=i.status==="ready"?i.clientId:null,d=i.status==="ready"?i.projectRoot:null,p=Rv({clientId:f,sessionId:s}),T=u0(i,p),_=t0(s),C=e0({isReadOnly:T,sessionId:s,widgetTreeState:_.widgetTreeState,setWidgetTreeState:_.setWidgetTreeState,refreshWidgetTree:_.refreshWidgetTree}),g=a0({minWidth:260,maxWidth:520,defaultWidth:360}),D=n0({isSelectWidgetActive:C.topBarActionState.isSelectWidgetActive,sessionId:s,widgetTreeState:_.widgetTreeState}),b=Uv(i);return S.jsxs("main",{className:"ask-ui-workbench",children:[S.jsx(yv,{hotReload:C.topBarActionState.hotReload,hotRestart:C.topBarActionState.hotRestart,isSelectWidgetActive:C.topBarActionState.isSelectWidgetActive,onHotReload:C.handleHotReload,onHotRestart:C.handleHotRestart,onToggleSelectWidget:C.handleToggleSelectWidget,statusMessage:pv(C.topBarActionState,b),targetDeviceDisplay:b}),S.jsxs("div",{className:"workbench-content",style:g.contentStyle,children:[S.jsx(Mv,{bridgeSessionState:i,onRefresh:_.refreshWidgetTree,onSelectedWidgetIdChange:D.handleSelectedWidgetIdChange,selectedWidgetId:D.selectedWidgetId,widgetTreeState:_.widgetTreeState}),S.jsx("div",{...g.resizeHandleProps}),S.jsx(iy,{isSelectWidgetActive:C.topBarActionState.isSelectWidgetActive,isInputDisabled:T,overlayMarkers:D.overlayMarkers,onDeviceControlMessage:r.sendDeviceControlMessage,onDeviceVideoRendererChange:r.setDeviceVideoRenderer,onRetry:r.retryLiveAppSurface,surfaceState:r.surfaceState,targetDeviceDisplay:b}),S.jsx(gv,{activeSelectionCommentId:D.activeSelectionCommentId,attachmentTokens:D.attachmentTokens,chatSessionState:p,isReadOnly:T,isSelectWidgetActive:C.topBarActionState.isSelectWidgetActive,onAttachmentTokenClick:D.handleAttachmentTokenClick,onSelectionCommentStateChange:D.setSelectionCommentState,selectedWidget:D.selectedWidget,selectionCommentState:D.selectionCommentState,sessionId:s,projectRoot:d,widgetTreeStatus:_.widgetTreeState.status})]})]})}$g.createRoot(document.getElementById("root")).render(S.jsx(q.StrictMode,{children:S.jsx(i0,{})})); diff --git a/apps/bridge/web/assets/index-Cf93crWF.css b/apps/bridge/web/assets/index-Cf93crWF.css new file mode 100644 index 0000000..63605eb --- /dev/null +++ b/apps/bridge/web/assets/index-Cf93crWF.css @@ -0,0 +1 @@ +.device-shell{display:grid;width:100%;height:100%;min-height:0;grid-template-rows:minmax(0,1fr) auto;gap:10px}.device-view-area{position:relative;min-height:260px;padding:0;overflow:hidden;border:0;border-radius:0;background:transparent;box-shadow:none;touch-action:none}.device-view-frame{position:absolute;top:0;left:0;display:grid;place-content:center;overflow:hidden;border-radius:10px;background:linear-gradient(180deg,rgba(255,255,255,.08),transparent),#182028;box-shadow:0 18px 40px #0f172a2e,0 0 0 1px #11182724;color:#edf7f2;text-align:center}.device-view-status{font-size:13px;font-weight:700}.device-view-device-id{max-width:min(220px,80%);margin-top:6px;overflow:hidden;color:#edf7f2ad;font-size:12px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.device-view-canvas{display:block;width:100%;height:100%}.selection-marker-layer{position:absolute;inset:0;z-index:3;pointer-events:none}.selection-marker{position:absolute;display:grid;place-items:center;border:2px solid #ffffff;border-radius:999px;background:var(--vue-green);box-shadow:0 8px 18px #0f172a38;color:#fff;font-size:12px;font-weight:900;line-height:1}.surface-controls{display:flex;align-items:center;justify-content:center;justify-self:center;gap:18px;width:min(100%,220px);min-height:42px;padding:6px 10px;border:1px solid rgba(17,24,39,.14);border-radius:999px;background:#111827eb;box-shadow:0 12px 28px #0f172a2e,inset 0 1px #ffffff14}.surface-controls button{display:inline-flex;align-items:center;justify-content:center;width:34px;height:30px;padding:0;border:0;border-radius:999px;background:transparent;color:#edf7f2db;font:inherit;line-height:1}.surface-controls button:not(:disabled){cursor:pointer}.surface-controls button:not(:disabled):hover,.surface-controls button:not(:disabled):focus-visible{background:#ffffff1a;color:#fff}.surface-controls button:focus-visible{outline:2px solid rgba(66,184,131,.62);outline-offset:2px}.surface-controls button:disabled{cursor:not-allowed;opacity:.48}.surface-control-icon{position:relative;display:block;width:18px;height:18px}.surface-control-icon-back:before{position:absolute;top:3px;left:5px;width:10px;height:10px;border-bottom:2px solid currentColor;border-left:2px solid currentColor;content:"";transform:rotate(45deg)}.surface-control-icon-home{width:15px;height:15px;border:2px solid currentColor;border-radius:999px}.surface-control-icon-recents:before,.surface-control-icon-recents:after{position:absolute;width:13px;height:13px;border:2px solid currentColor;border-radius:2px;content:""}.surface-control-icon-recents:before{top:2px;left:2px}.surface-control-icon-recents:after{display:none}.live-app-surface{display:flex;align-items:center;justify-content:center;background:linear-gradient(90deg,rgba(66,184,131,.035) 1px,transparent 1px),linear-gradient(180deg,rgba(66,184,131,.035) 1px,transparent 1px),#f4faf7;background-size:32px 32px;color:var(--muted)}.live-app-surface-selecting{background:linear-gradient(90deg,rgba(66,184,131,.07) 1px,transparent 1px),linear-gradient(180deg,rgba(66,184,131,.07) 1px,transparent 1px),#effaf4}.live-app-surface-waiting-stack{position:relative;display:grid;place-items:center;width:100%;height:100%;min-height:0}.live-app-surface-hidden-device-shell{position:absolute;inset:0;display:grid;place-items:center;opacity:0;pointer-events:none}.live-app-phone-state-shell{display:grid;width:min(82%,360px);max-height:calc(100% - 40px)}.live-app-phone-state-hardware{position:relative;aspect-ratio:9 / 19.5;min-height:360px;max-height:720px;padding:14px;border-radius:32px;background:linear-gradient(135deg,rgba(255,255,255,.1),transparent 30%),#121a22;box-shadow:0 24px 60px #0f172a3d,inset 0 0 0 1px #ffffff14}.live-app-phone-state-speaker{position:absolute;top:9px;left:50%;width:54px;height:4px;border-radius:999px;background:#ffffff29;transform:translate(-50%)}.live-app-phone-state-screen{position:relative;width:100%;height:100%;overflow:hidden;border-radius:22px;background:#eef4f7;color:#24384a}.live-app-phone-status-bar{position:absolute;z-index:3;top:0;left:0;display:flex;align-items:center;justify-content:space-between;width:100%;height:34px;padding:0 18px;color:#1a2a37c7;font-size:11px;font-weight:800}.live-app-phone-skeleton{display:grid;gap:14px;height:100%;padding:58px 20px 24px;background:linear-gradient(180deg,rgba(66,184,131,.1),transparent 42%),#f8fcfa}.live-app-phone-skeleton-hero,.live-app-phone-skeleton-line,.live-app-phone-skeleton-grid div{border-radius:8px;background:linear-gradient(90deg,#889fae29,#42b88333,#889fae29)}.live-app-phone-skeleton-hero{height:82px}.live-app-phone-skeleton-line{width:68%;height:14px}.live-app-phone-skeleton-line-wide{width:92%}.live-app-phone-skeleton-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px}.live-app-phone-skeleton-grid div{aspect-ratio:1}.live-app-phone-state-overlay{position:absolute;z-index:4;inset:0;display:grid;place-content:center;gap:9px;padding:28px;background:#f8fcfab8;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:#1f3446;text-align:center}.live-app-phone-spinner{justify-self:center;width:34px;height:34px;border:3px solid rgba(66,184,131,.22);border-top-color:var(--vue-green);border-radius:999px;animation:live-app-phone-spinner-spin .9s linear infinite}@keyframes live-app-phone-spinner-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.live-app-phone-spinner{animation:none}}.live-app-phone-state-title{color:inherit;font-size:15px;font-weight:850}.live-app-phone-state-detail{max-width:230px;overflow:hidden;color:#4e6070d1;font-size:12px;font-weight:650;line-height:1.45;text-overflow:ellipsis}.live-app-surface-retry{margin-top:4px;padding:7px 14px;border:1px solid rgba(66,184,131,.28);border-radius:999px;background:#fff;color:var(--vue-green-dark);font:inherit;font-size:12px;font-weight:750}.live-app-surface-retry:not(:disabled){cursor:pointer}.live-app-surface-selecting .live-app-phone-state-hardware{box-shadow:0 0 0 3px #42b88324,0 24px 60px #0f172a3d,inset 0 0 0 1px #ffffff14}.live-app-device-stage{position:relative;display:grid;width:min(100%,390px);height:min(100%,820px);min-height:0}.chat-panel{display:grid;grid-template-rows:auto minmax(240px,38%) minmax(0,1fr) auto;border-left:1px solid var(--border);overflow:hidden}.chat-panel-header{display:grid;gap:8px;padding:12px;border-bottom:1px solid var(--border);background:#fffffff0}.chat-panel-title{color:var(--vue-ink);font-size:14px;font-weight:850}.agent-status{display:inline-grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;gap:6px;min-width:0;color:var(--muted);font-size:11px;font-weight:700}.agent-status-dot{width:7px;height:7px;border-radius:999px;background:#9aa8b8;box-shadow:0 0 0 3px #64748b1f}.agent-status-label{color:#526274}.agent-status-value{min-width:0;overflow:hidden;color:var(--vue-ink);text-overflow:ellipsis;white-space:nowrap}.selected-widget-card{margin:12px;padding:12px;border:1px solid rgba(66,184,131,.18);border-radius:8px;background:var(--surface-soft);overflow:auto}.selected-widget-content{display:grid;gap:10px;margin-top:8px}.selected-widget-summary{display:grid;gap:6px;min-width:0}.selected-widget-name{min-width:0;overflow:hidden;color:var(--vue-ink);font-size:13px;font-weight:850;text-overflow:ellipsis;white-space:nowrap}.selected-widget-meta{min-width:0;overflow:hidden;color:var(--muted);font-size:11px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.selected-widget-detail{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:5px;align-items:start;color:var(--muted);font-size:11px;font-weight:700;line-height:1.35}.selected-widget-detail span{white-space:nowrap}.selected-widget-detail strong{min-width:0;overflow-wrap:anywhere;color:var(--vue-ink);font-weight:750}.selection-comment-list{display:grid;gap:7px}.selection-comment-empty{padding:8px 10px;border:1px dashed rgba(100,116,139,.26);border-radius:7px;color:var(--muted);font-size:11px;font-weight:650}.selection-comment-item{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:6px;align-items:start}.selection-comment-number{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:999px;background:#42b88324;color:var(--vue-green-dark);font-size:11px;font-weight:850}.selection-comment-edit,.selection-comment-input{width:100%;min-width:0;padding:8px 9px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:#fff;color:var(--vue-ink);font:inherit;font-size:12px;font-weight:600;line-height:1.4;resize:vertical}.selection-comment-edit{min-height:48px;resize:none}.selection-comment-input{min-height:68px;max-height:120px}.selection-comment-delete,.selection-comment-add{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:0 10px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:#fff;color:var(--vue-green-dark);font:inherit;font-size:11px;font-weight:800}.selection-comment-delete:not(:disabled),.selection-comment-add:not(:disabled){cursor:pointer}.selection-comment-add{background:var(--vue-green);color:#fff}.selection-comment-add:disabled{cursor:not-allowed;opacity:.5}.selection-comment-composer{display:grid;gap:7px}.selection-comment-footer{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.selection-comment-disabled-reason{min-width:0;color:var(--muted);font-size:11px;font-weight:650;line-height:1.35;overflow-wrap:anywhere}.chat-section-title{color:var(--vue-ink);font-size:12px;font-weight:800}.chat-empty-state{margin:8px 0 0;color:var(--muted);font-size:12px;font-weight:600;line-height:1.45}.chat-history{position:relative;min-height:0;padding:0 12px 12px;overflow:auto}.chat-history-empty{display:grid;place-items:center;min-height:132px;margin-top:8px;border:1px dashed rgba(100,116,139,.28);border-radius:8px;color:var(--muted);font-size:12px;font-weight:650;text-align:center}.chat-connection-warning{margin-top:8px;padding:8px 10px;border:1px solid rgba(199,71,71,.24);border-radius:7px;background:#fff8f7;color:#9f312d;font-size:11px;font-weight:700;line-height:1.35}.chat-history-list{display:grid;gap:8px;margin:8px 0 0;padding:0;list-style:none}.chat-history-message{display:grid;gap:5px;padding:10px;border:1px solid rgba(66,184,131,.16);border-radius:8px;background:#fff}.chat-history-message-role{color:var(--muted);font-size:10px;font-weight:850;text-transform:uppercase}.chat-history-message-text{color:var(--vue-ink);font-size:12px;font-weight:600;line-height:1.45;overflow-wrap:anywhere;white-space:pre-wrap}.chat-history-attachment-list{display:grid;gap:6px;margin:0;padding:0;list-style:none}.chat-history-attachment{display:grid;gap:4px;padding:8px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:#f8fbfa}.chat-history-attachment-heading{display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px;align-items:center;min-width:0;color:var(--vue-ink);font-size:11px;font-weight:850}.chat-history-attachment-number{color:var(--vue-green-dark)}.chat-history-attachment-widget{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-history-attachment-text{color:var(--vue-ink);font-size:12px;font-weight:600;line-height:1.45;overflow-wrap:anywhere;white-space:pre-wrap}.chat-history-message-working{border-style:dashed;background:#f8fbfa}.chat-history-attachment-meta{display:flex;flex-wrap:wrap;gap:6px;min-width:0;color:var(--muted);font-size:10px;font-weight:750;line-height:1.35;overflow-wrap:anywhere}.chat-history-new-message{position:sticky;bottom:8px;justify-self:center;display:block;width:fit-content;margin:8px auto 0;padding:6px 10px;border:1px solid rgba(66,184,131,.24);border-radius:999px;background:var(--vue-green);color:#fff;font:inherit;font-size:11px;font-weight:850;box-shadow:0 8px 18px #1f29371f;cursor:pointer}.chat-composer{display:grid;gap:8px;padding:12px;border-top:1px solid var(--border);background:#fffffff5}.attachment-token-list{display:flex;flex-wrap:wrap;gap:6px;min-width:0}.attachment-token{display:inline-grid;grid-template-columns:auto minmax(0,1fr);align-items:center;max-width:100%;min-height:28px;padding:0 9px;border:1px solid rgba(66,184,131,.22);border-radius:7px;background:#fff;color:var(--vue-ink);cursor:pointer;font:inherit;font-size:11px;font-weight:800;gap:6px}.attachment-token-number{color:var(--vue-green-dark)}.attachment-token-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.attachment-token-unavailable{border-style:dashed;color:#7a8794}.chat-composer-input{width:100%;min-height:72px;max-height:136px;padding:9px 10px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:#f8fbfa;color:var(--vue-ink);font:inherit;font-size:12px;font-weight:600;line-height:1.4;resize:none}.chat-composer-input::placeholder{color:#8a99a8}.chat-composer-input:disabled{cursor:not-allowed;opacity:.78}.chat-composer-footer{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.chat-composer-disabled-reason{min-width:0;color:var(--muted);font-size:11px;font-weight:650;line-height:1.35;overflow-wrap:anywhere}.chat-send-button{display:inline-flex;align-items:center;justify-content:center;min-width:58px;min-height:30px;padding:0 12px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:var(--vue-green);color:#fff;font:inherit;font-size:12px;font-weight:800}.chat-send-button:disabled{cursor:not-allowed;opacity:.5}.top-bar{display:flex;align-items:center;gap:12px;height:52px;padding:0 18px;border-bottom:1px solid var(--border);background:#ffffffeb;box-shadow:0 1px #35495e0a;font-size:14px;font-weight:700;letter-spacing:0;overflow:hidden;white-space:nowrap}.top-bar-brand{display:flex;align-items:center;flex:0 0 auto;gap:10px}.brand-mark{width:12px;height:12px;border-radius:4px;background:var(--vue-green);box-shadow:0 0 0 4px #42b8831f,inset 0 -2px #2f8f6659}.top-bar-session{display:inline-flex;align-items:center;flex:0 1 auto;gap:8px;min-width:0;max-width:320px;padding:6px 10px;border:1px solid rgba(66,184,131,.16);border-radius:8px;background:#f7fbf9e6;color:var(--vue-ink);font-size:12px;font-weight:650}.status-dot{flex:0 0 auto;width:7px;height:7px;border-radius:999px;background:var(--vue-green);box-shadow:0 0 0 3px #42b88324}.session-muted{min-width:0;overflow:hidden;color:var(--muted);font-weight:600;text-overflow:ellipsis}.target-device-label{min-width:0;overflow:hidden;text-overflow:ellipsis}.toolbar-button:not(:disabled){cursor:pointer}.top-bar-actions{display:flex;align-items:center;justify-content:flex-end;flex:1 1 auto;gap:8px;min-width:0;overflow:hidden}.toolbar-button{display:inline-flex;align-items:center;justify-content:center;min-height:30px;max-width:140px;padding:0 11px;border:1px solid rgba(53,73,94,.1);border-radius:7px;background:#fff;color:var(--vue-ink);font:inherit;font-size:12px;font-weight:700;letter-spacing:0;line-height:1;overflow:hidden;text-overflow:ellipsis}.toolbar-button:disabled{cursor:not-allowed;opacity:.5}.toolbar-button-active{border-color:#42b88359;background:var(--vue-green);color:#fff;box-shadow:0 0 0 3px #42b88324,0 6px 14px #42b8832e}.toolbar-button-error{border-color:#c747473d;color:#9a3d3d}.widget-tree-panel{background:#fbfefd}.widget-tree-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px;border-bottom:1px solid var(--border);background:#fbfefdf0}.widget-tree-icon-button{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:30px;height:30px;border:1px solid rgba(66,184,131,.2);border-radius:7px;background:#fff;color:var(--vue-green-dark);font:inherit;font-size:16px;font-weight:800;line-height:1}.widget-tree-icon-button:hover,.widget-tree-icon-button:focus-visible{border-color:#42b8836b;background:var(--surface-soft)}.widget-tree-icon-button:disabled{cursor:not-allowed;opacity:.45}.widget-tree-title{color:var(--vue-ink);font-size:13px;font-weight:800}.widget-tree-subtitle{margin-top:2px;color:var(--muted);font-size:11px;font-weight:650}.widget-tree-search{position:sticky;top:55px;z-index:1;display:grid;grid-template-columns:18px minmax(0,1fr) auto auto;align-items:center;gap:7px;padding:8px 12px;border-bottom:1px solid var(--border);background:#fbfefdf0}.widget-tree-search-icon{color:var(--muted);font-size:14px;font-weight:800;line-height:1;text-align:center}.widget-tree-search-input{min-width:0;height:30px;padding:0 10px;border:1px solid rgba(66,184,131,.18);border-radius:7px;background:#fff;color:var(--vue-ink);font:inherit;font-size:12px;font-weight:650;outline:none}.widget-tree-search-input::placeholder{color:#8a99a8}.widget-tree-search-input:focus{border-color:#42b8838c;box-shadow:0 0 0 3px #42b8831f}.widget-tree-search-counter{min-width:34px;color:var(--muted);font-size:11px;font-weight:800;text-align:right}.widget-tree-search-actions{display:inline-flex;align-items:center;gap:2px}.widget-tree-search-action{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--vue-ink);cursor:pointer;font:inherit;font-size:13px;font-weight:900;line-height:1}.widget-tree-search-action:before{content:"";display:block;width:7px;height:7px;border-top:2px solid currentColor;border-left:2px solid currentColor}.widget-tree-search-action-previous:before{transform:translateY(2px) rotate(45deg)}.widget-tree-search-action-next:before{transform:translateY(-2px) rotate(225deg)}.widget-tree-search-action:hover,.widget-tree-search-action:focus-visible{border-color:#42b88342;background:#42b8831a;outline:none}.widget-tree-search-action:disabled{cursor:not-allowed;opacity:.38}.widget-tree-selection-error{margin:0 12px 8px;padding:8px 10px;border:1px solid rgba(199,71,71,.24);border-radius:7px;background:#fff8f7;color:#9f312d;font-size:11px;font-weight:700;line-height:1.35}.widget-tree-list{margin:0;padding:0;list-style:none}.widget-tree-root-list{padding:8px 8px 18px}.widget-tree-flat-list{display:grid;gap:1px}.widget-tree-state{margin:12px;padding:12px;border:1px solid rgba(66,184,131,.18);border-radius:8px;background:var(--surface-soft)}.widget-tree-state-error{border-color:#c747473d;background:#fff8f7}.widget-tree-state-title{color:var(--vue-ink);font-size:13px;font-weight:800}.widget-tree-state-copy{margin-top:6px;color:var(--muted);font-size:12px;font-weight:600;line-height:1.45}.widget-tree-row{--tree-indent: calc(var(--tree-depth) * 12px);position:relative;display:grid;grid-template-columns:14px 12px 20px minmax(0,1fr);align-items:center;min-height:28px;margin:1px 0;padding:2px 8px 2px calc(4px + var(--tree-indent));border:1px solid transparent;border-radius:7px;color:#62748a;cursor:pointer;font-size:13px;font-weight:600}.widget-tree-row:hover,.widget-tree-row:focus-visible{border-color:#42b88347;background:#42b88314;outline:none}.widget-tree-row-app{color:var(--vue-ink);font-weight:750}.widget-tree-row-selected{border-color:#42b8836b;background:#42b8832e;box-shadow:inset 3px 0 0 var(--vue-green),0 6px 16px #35495e14;color:#18392b}.widget-tree-row-search-active{border-color:#42b88394;background:#42b8833d;box-shadow:inset 3px 0 0 var(--vue-green),0 6px 18px #42b88329;color:#143325}.widget-tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:14px;height:22px;padding:0;border:0;background:transparent;color:#6b7c8f;cursor:pointer;font:inherit;font-size:10px;font-weight:900;line-height:1;text-align:center}.widget-tree-toggle:disabled{cursor:default}.widget-tree-toggle:focus-visible{border-radius:5px;outline:2px solid rgba(66,184,131,.38);outline-offset:-1px}.widget-tree-branch{width:10px;height:18px;border-left:1px solid rgba(53,73,94,.22);border-bottom:1px solid rgba(53,73,94,.22);transform:translateY(-5px)}.widget-tree-row-selected .widget-tree-branch{border-color:#42b883ad}.widget-tree-type-icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5px;color:#53657a;font-size:13px;font-weight:850;line-height:1}.widget-tree-type-icon-custom,.widget-tree-type-icon-app{color:var(--vue-green-dark)}.widget-tree-type-icon-layout{color:#405165}.widget-tree-type-icon-gesture{color:#8a6a3f}.widget-tree-type-icon-visual{color:#5d6590}.widget-tree-type-icon-animation{color:#8a5b70}.widget-tree-type-icon-text{color:#536d8c;font-size:11px}.widget-tree-type-icon-widget{color:#8a99a8}.widget-tree-row-selected .widget-tree-type-icon{color:var(--vue-green-dark)}.widget-tree-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ask-ui-workbench{display:grid;grid-template-rows:52px minmax(0,1fr);width:100vw;height:100vh;background:linear-gradient(180deg,#42b88314,#42b88300),#eef7f2}.workbench-content{display:grid;grid-template-columns:var(--widget-tree-width, 360px) 6px minmax(0,1fr) 340px;min-height:0;overflow:hidden}.workbench-panel{min-width:0;min-height:0;overflow:auto;background:var(--surface)}.panel-resize-handle{position:relative;z-index:2;width:6px;min-width:6px;border-right:1px solid var(--border);border-left:1px solid var(--border);background:#d9e7df8c;cursor:col-resize;touch-action:none}.panel-resize-handle:after{position:absolute;top:50%;left:50%;width:2px;height:36px;border-radius:999px;background:#42b88357;content:"";opacity:0;transform:translate(-50%,-50%);transition:opacity .12s ease,background .12s ease}.panel-resize-handle:hover:after,.panel-resize-handle:focus-visible:after,.panel-resize-handle:active:after{background:var(--vue-green);opacity:1}.panel-resize-handle:focus-visible{outline:2px solid rgba(66,184,131,.38);outline-offset:-2px}:root{--vue-green: #42b883;--vue-green-dark: #2f8f66;--vue-ink: #35495e;--surface: #ffffff;--surface-soft: #f7fbf9;--border: #d9e7df;--muted: #64748b;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:var(--vue-ink);background:#eef7f2}*{box-sizing:border-box}html,body,#root{width:100%;height:100%;margin:0}body{overflow:hidden} diff --git a/apps/bridge/web/index.html b/apps/bridge/web/index.html new file mode 100644 index 0000000..56deb0d --- /dev/null +++ b/apps/bridge/web/index.html @@ -0,0 +1,13 @@ + + + + + + Ask UI + + + + +
+ + diff --git a/docs/product/ask-ui-launch-prd.md b/docs/product/ask-ui-launch-prd.md index 39b8f46..0d31097 100644 --- a/docs/product/ask-ui-launch-prd.md +++ b/docs/product/ask-ui-launch-prd.md @@ -6,7 +6,7 @@ Ask UI is intended to be started from the same Codex, Claude Code, or similar co That manual chain is fragile. It exposes implementation details such as the React/Vite development server to installed users, makes the Agent Session depend on browser-created session state, and leaves too much room for mismatched device, flavor, VM Service, Bridge Session, and poll command parameters. -Ask UI needs a single launch contract that coding agents can run reliably. The installed-user path should work from a Flutter project with an `ask_ui_bridge` dev dependency, serve the packaged Web workbench from the local bridge, open the browser, and return the exact Agent Session Command needed to keep receiving Chat messages from Ask UI. +Ask UI needs a single launch contract that coding agents can run reliably. The installed-user path should work from a Flutter project that integrates `ask_ui_runtime`, use a globally activated `ask_ui_bridge` CLI, serve the packaged Web workbench from the local bridge, open the browser, and return the exact Agent Session Command needed to keep receiving Chat messages from Ask UI. ## Solution @@ -17,19 +17,20 @@ The launch command discovers or accepts a Target Device, supports Flutter flavor Installed users should run Ask UI through the pub package: - Add the runtime package to the Flutter app. -- Add the bridge package as a dev dependency. -- Run the bridge executable through Dart. +- Register the runtime before `runApp`. +- Globally activate the bridge package. +- Run the `ask_ui_bridge` executable from the Flutter project. The React/TypeScript Web app remains the implementation of the workbench, but users do not install or run the Web source. Release preparation builds the Web app into static files and includes those files inside the bridge package artifact. The Bridge Server serves those static files from the same local origin as the API. -The coding-agent skill stays thin. It calls `launch`, handles `needs_device_selection` JSON by asking the user to choose a device, runs the returned Agent Session Command when launch is ready, treats polled Ask UI messages as current task input, writes replies through the Agent Session Command, and continues polling until the user stops. +The coding-agent skills stay thin. They call `launch`, handle `needs_device_selection` JSON by asking the user to choose a device, run the returned Agent Session Command when launch is ready, treat polled Ask UI messages as current task input, write replies through the Agent Session Command, and continue polling until the user stops. ## User Stories 1. As a Flutter developer, I want to start Ask UI with one command from my coding-agent session, so that I do not manually coordinate several local processes. 2. As a Flutter developer, I want the launch command to use the same Agent Session that will later edit code, so that UI feedback returns to the right coding agent. -3. As a Flutter developer, I want the installed-user command to run from my Flutter project, so that Ask UI startup is project-local and reproducible. -4. As a Flutter developer, I want to install the bridge as a dev dependency, so that the project controls the Ask UI bridge version. +3. As a Flutter developer, I want the installed-user command to run from my Flutter project, so that Ask UI starts against the app I am inspecting. +4. As a Flutter developer, I want the bridge as a globally activated development CLI, so that my app only needs the runtime integration. 5. As a Flutter developer, I want the runtime package to remain separate from the bridge dev tool, so that production app dependencies stay minimal. 6. As a Flutter developer, I want launch to discover Flutter devices when I do not specify one, so that I can choose from the devices currently visible to Flutter. 7. As a Flutter developer, I want launch to proceed automatically when exactly one usable device exists, so that common single-device setups are fast. @@ -67,7 +68,7 @@ The coding-agent skill stays thin. It calls `launch`, handles `needs_device_sele 39. As a coding agent, I want launch success output to include a clear next step, so that I run the Agent Session Command rather than replying too early. 40. As a coding agent, I want launch failures to use stable error codes, so that I can decide whether to ask the user, retry, or stop. 41. As a coding agent, I want the launch skill to stay thin, so that fragile process orchestration lives in tested CLI code. -42. As a coding agent, I want the skill to prefer the installed bridge command, so that project-local package versions are honored. +42. As a coding agent, I want the skill to prefer the installed bridge command, so that the documented CLI contract is honored. 43. As a coding agent, I want the skill to support the monorepo development fallback, so that Ask UI can dogfood its own source checkout. 44. As a coding agent, I want the skill to ask the user only when device selection is needed, so that startup remains mostly automatic. 45. As a coding agent, I want polled Ask UI messages to become current task input, so that selected widgets, comments, and snapshots drive normal code edits. @@ -79,8 +80,8 @@ The coding-agent skill stays thin. It calls `launch`, handles `needs_device_sele 51. As a Flutter developer, I want launch to preserve existing Bridge Session API behavior, so that current Web features continue working. 52. As a Flutter developer, I want existing URL bootstrap by VM Service, project root, and device id to keep working, so that current development and tests are not broken. 53. As a Flutter developer, I want the new session attach URL to be additive, so that launch can improve startup without removing the older bootstrap path. -54. As a Flutter developer, I want launch to avoid global installation as the default, so that version mismatch between runtime, bridge, and skill is less likely. -55. As a Flutter developer, I want a global command to remain possible later, so that convenience installs can be added without changing the core contract. +54. As a Flutter developer, I want the bridge CLI to stay separate from my app dependencies, so that production dependency graphs stay focused on runtime code. +55. As a Flutter developer, I want the global bridge command to be explicit, so that installation and upgrade behavior is easy to explain. 56. As an Ask UI maintainer, I want launch modules to be deep and testable, so that device discovery, Flutter startup, static serving, and browser opening can evolve independently. 57. As an Ask UI maintainer, I want packaging validation, so that releases do not accidentally omit the Web workbench. 58. As an Ask UI maintainer, I want launch to match the existing Agent Chat Long-Poll model, so that browser feedback keeps flowing through the original Agent Session. @@ -89,11 +90,12 @@ The coding-agent skill stays thin. It calls `launch`, handles `needs_device_sele - Add `launch` as a bridge CLI command alongside the existing server start path and Agent Session Command. - Keep `agent poll` as the canonical command for receiving Ask UI Chat messages and writing replies or command-level errors. -- Make the installed-user default command project-local through Dart package execution. -- Treat global installation as optional convenience, not the default skill path. +- Make the installed-user default command the globally activated `ask_ui_bridge` executable. +- Keep the source entrypoint as the Ask UI maintainer development fallback. - Keep the coding-agent skill thin; it calls the launcher and follows returned next-step instructions. - Use machine-readable JSON as the launch command output contract. -- Use stable launch statuses such as ready, needs-device-selection, and error. +- Use stable launch statuses such as `ready`, `needs_device_selection`, and + `error`. - Include stable launch error codes for missing devices, ambiguous device selection, Flutter startup failure, bridge startup failure, session creation failure, packaged Web missing, browser open failure where appropriate, and invalid arguments. - Discover devices through Flutter's machine-readable device listing. - Match explicit device ids exactly after trimming. @@ -157,7 +159,7 @@ The coding-agent skill stays thin. It calls `launch`, handles `needs_device_sele - Multiple active Agent Session pollers for one Bridge Session. - Automatic installation of Flutter, Dart, Node, or platform device tooling. - Project-specific flavor-to-target configuration in the first version. -- Global CLI installation as the default documented skill path. +- Project-local `ask_ui_bridge` dev dependency as the default documented skill path. - Detached daemon lifecycle, launch status, and launch stop commands. - Automatic recovery from Flutter process crashes after a successful launch. - Port scanning by the Agent Session Command. diff --git a/issues/0.0.4-ask-ui-launch.md b/issues/0.0.4-ask-ui-launch.md index bae2a2b..17e67e6 100644 --- a/issues/0.0.4-ask-ui-launch.md +++ b/issues/0.0.4-ask-ui-launch.md @@ -157,6 +157,8 @@ After the Bridge Session exists, launch should construct a workbench URL served Type: AFK +Status: Done + ## What to build Support a development-only Web mode for Ask UI contributors while keeping packaged Web as the default installed-user path. @@ -165,14 +167,14 @@ In the Ask UI monorepo, `launch --web-dev` should start or reuse the Vite dev se ## Acceptance criteria -- [ ] `launch --web-dev` is available for monorepo development. -- [ ] Web dev mode starts the Vite dev server from the Web app package. -- [ ] Web dev mode captures the actual Vite URL, including alternate ports. -- [ ] Web dev mode opens the Vite workbench URL with session attach parameters. -- [ ] Default launch behavior uses packaged Web rather than Vite. -- [ ] If packaged Web is missing and `--web-dev` was not requested, launch returns a clear JSON error. -- [ ] Web dev mode is not required for installed users. -- [ ] Tests cover mode selection, Vite URL parsing, fallback errors, and generated Web dev URLs. +- [x] `launch --web-dev` is available for monorepo development. +- [x] Web dev mode starts the Vite dev server from the Web app package. +- [x] Web dev mode captures the actual Vite URL, including alternate ports. +- [x] Web dev mode opens the Vite workbench URL with session attach parameters. +- [x] Default launch behavior uses packaged Web rather than Vite. +- [x] If packaged Web is missing and `--web-dev` was not requested, launch returns a clear JSON error. +- [x] Web dev mode is not required for installed users. +- [x] Tests cover mode selection, Vite URL parsing, fallback errors, and generated Web dev URLs. ## Blocked by @@ -182,22 +184,28 @@ In the Ask UI monorepo, `launch --web-dev` should start or reuse the Vite dev se Type: AFK +Status: Done + +Implemented in `d9f8955 chore: prepare bridge package release`. + ## What to build -Prepare `ask_ui_bridge` for pub package installation as the default user-facing launcher. +Prepare `ask_ui_bridge` for pub global activation as the default user-facing launcher. -The bridge package should expose an executable so Flutter projects can add `ask_ui_bridge` as a dev dependency and run `dart run ask_ui_bridge launch`. Release preparation should build the Web app, copy the static output into the bridge package, and keep generated or local-only files out of normal development commits except where they are required for the pub artifact. +The bridge package should expose an executable so Flutter developers can globally activate `ask_ui_bridge` and run `ask_ui_bridge launch` from the Flutter project they are inspecting. Release preparation should build the Web app, copy the static output into the bridge package, and keep generated or local-only files out of normal development commits except where they are required for the pub artifact. ## Acceptance criteria -- [ ] The bridge package declares an executable for `dart run ask_ui_bridge`. -- [ ] Package metadata is suitable for pub publishing. -- [ ] The release build runs the Web build before publishing. -- [ ] The release build copies Web build output into the bridge package static directory. -- [ ] Publish ignore rules do not exclude files required by the packaged workbench. -- [ ] Development-only artifacts such as `node_modules`, Vite build info, and local cache files remain excluded. -- [ ] Documentation or command help shows the installed-user command `dart run ask_ui_bridge launch`. -- [ ] Tests or validation scripts cover the release build's expected packaged file layout. +- [x] The bridge package declares an executable for `ask_ui_bridge`. +- [x] Package metadata is suitable for pub publishing. +- [x] The release build runs the Web build before publishing. +- [x] The release build copies Web build output into the bridge package static directory. +- [x] Publish ignore rules do not exclude files required by the packaged workbench. +- [x] Development-only artifacts such as `node_modules`, Vite build info, and local cache files remain excluded. +- [x] Documentation or command help shows the installed-user command `ask_ui_bridge launch`. +- [x] Tests or validation scripts cover the release build's expected packaged file layout. +- [x] CI validates the bridge release package by building Web, checking the packaged file layout, and running `dart pub publish --dry-run`. +- [x] A tag-triggered publish workflow can publish `ask_ui_bridge` to pub.dev after the same release validation passes. ## Blocked by @@ -210,23 +218,23 @@ Type: AFK ## What to build -Add the project skill workflow that lets Codex, Claude Code, and similar coding-agent sessions start Ask UI and enter the Agent Session Command loop. +Add the project skill workflows that let Codex, Claude Code, and similar coding-agent sessions start Ask UI and enter the Agent Session Command loop. -The skill should stay thin. It should call the launcher, handle device selection results, run the returned agent poll command, process Ask UI messages as normal agent tasks, write replies through the Agent Session Command, and continue polling until the session ends or the user asks to stop. +The skills should stay thin. The normal `ask-ui` skill should call the globally activated launcher. The maintainer `ask-ui-dev` skill should call the source launcher and document optional Web development mode. Both should handle device selection results, run the returned agent poll command, process Ask UI messages as normal agent tasks, write replies through the Agent Session Command, and continue polling until the session ends or the user asks to stop. ## Acceptance criteria -- [ ] The skill instructs agents to prefer `dart run ask_ui_bridge launch`. -- [ ] The skill documents the monorepo development fallback command. -- [ ] The skill explains how to pass optional device and flavor arguments. -- [ ] The skill handles `needs_device_selection` by asking the user to choose and rerunning launch with the selected device. -- [ ] The skill handles `ready` by running the returned agent command. -- [ ] The skill treats returned poll messages as current-session task input. -- [ ] The skill tells the agent to reply with `agent poll --reply-to --agent-reply `. -- [ ] The skill tells the agent to report command-level workflow errors with `--agent-error` when appropriate. -- [ ] The skill tells the agent to keep polling after each reply unless the user asks to stop. -- [ ] The skill does not duplicate fragile Flutter, bridge, Web, or browser orchestration details that belong in the CLI. -- [ ] Validation covers the documented launch, device selection, poll, reply, and continue-poll paths. +- [x] The normal skill instructs agents to prefer `ask_ui_bridge launch`. +- [x] The maintainer skill documents the monorepo development fallback command. +- [x] The skills explain how to pass optional device and flavor arguments. +- [x] The skills handle `needs_device_selection` by asking the user to choose and rerunning launch with the selected device. +- [x] The skills handle `ready` by running the returned agent command. +- [x] The skills treat returned poll messages as current-session task input. +- [x] The skills tell the agent to reply with `agent poll --reply-to --agent-reply `. +- [x] The skills tell the agent to report command-level workflow errors with `--agent-error` when appropriate. +- [x] The skills tell the agent to keep polling after each reply unless the user asks to stop. +- [x] The skills do not duplicate fragile Flutter, bridge, Web, or browser orchestration details that belong in the CLI. +- [x] Validation covers the documented launch, device selection, poll, reply, and continue-poll paths. ## Blocked by diff --git a/packages/ask_ui_runtime/CHANGELOG.md b/packages/ask_ui_runtime/CHANGELOG.md new file mode 100644 index 0000000..7b0fc84 --- /dev/null +++ b/packages/ask_ui_runtime/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 0.0.1 + +- Initial release of Ask UI runtime service extensions for Flutter app + integration. diff --git a/packages/ask_ui_runtime/LICENSE b/packages/ask_ui_runtime/LICENSE new file mode 100644 index 0000000..beec3ea --- /dev/null +++ b/packages/ask_ui_runtime/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ask UI contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/ask_ui_runtime/README.md b/packages/ask_ui_runtime/README.md index 7cdc610..bcd5228 100644 --- a/packages/ask_ui_runtime/README.md +++ b/packages/ask_ui_runtime/README.md @@ -8,8 +8,7 @@ Add the package to your Flutter app: ```yaml dependencies: - ask_ui_runtime: - path: /path/to/ask_ui/packages/ask_ui_runtime + ask_ui_runtime: ^0.0.1 ``` Register the runtime before `runApp`: diff --git a/packages/ask_ui_runtime/pubspec.yaml b/packages/ask_ui_runtime/pubspec.yaml index 253fe42..4b78b58 100644 --- a/packages/ask_ui_runtime/pubspec.yaml +++ b/packages/ask_ui_runtime/pubspec.yaml @@ -1,7 +1,8 @@ name: ask_ui_runtime description: Debug runtime service extensions for Ask UI Flutter app integration. -version: 0.1.0 -publish_to: none +version: 0.0.1 +repository: https://github.com/drown0315/ask_ui +homepage: https://github.com/drown0315/ask_ui environment: sdk: '>=3.4.0 <4.0.0' diff --git a/skills/ask-ui-dev/SKILL.md b/skills/ask-ui-dev/SKILL.md new file mode 100644 index 0000000..3d4f501 --- /dev/null +++ b/skills/ask-ui-dev/SKILL.md @@ -0,0 +1,122 @@ +--- +name: ask-ui-dev +description: Start Ask UI from the source checkout while developing the Ask UI repository, with optional Web dev server support. +--- + +# Ask UI Dev + +Use this skill when working inside the Ask UI repository itself to dogfood the +launcher, bridge, packaged workbench, or React/Vite workbench. + +Normal Flutter app users should install the `ask-ui` skill instead. + +## Start Ask UI From Source + +From `apps/bridge`, run the source entrypoint: + +```sh +dart run bin/ask_ui_bridge.dart launch +``` + +Pass Flutter launch options through the launcher when the test project needs a +specific target: + +```sh +dart run bin/ask_ui_bridge.dart launch \ + --device \ + --flavor \ + --target \ + --dart-define \ + --project-root +``` + +Use packaged Web by default when validating the release-style path. + +Add `--web-dev` only when developing the React/Vite workbench itself and Vite +hot reload is useful: + +```sh +dart run bin/ask_ui_bridge.dart launch --web-dev +``` + +## Handle Launch Output + +Read the launch command JSON from stdout. Keep process logs and diagnostics out +of JSON parsing decisions unless the command exits with an error. + +If `status` is `needs_device_selection`, ask the user to choose from the +returned devices. Use the matching `suggestedCommand` for the selected device so +the launcher preserves flavor, target, Dart defines, project root, open/no-open, +Web development intent, and other launch intent. Do not guess when the launcher +asks for device selection. + +If `status` is `ready`, run the returned `agentCommand` exactly. This command +enters the Agent Session Command loop for the Bridge Session that the workbench +is using. + +If `status` is `error`, explain the stable launch error to the user. Retry only +when the user has fixed the underlying problem or explicitly asks you to retry. + +## Process Ask UI Messages + +Treat each message returned by `agent poll` as current-session task input. The +message may include selected widgets, comments, snapshots, or other workbench +context. Handle it like any other user request in the active coding-agent +session: inspect the project, edit files when needed, run appropriate +verification, and prepare a concise response. + +Reply through the Agent Session Command so Chat History stays synchronized with +the Ask UI workbench: + +```sh +agent poll --reply-to --agent-reply +``` + +Use the full returned command shape when it includes bridge connection flags. +The launcher normally returns the installed executable form: + +```sh +ask_ui_bridge agent poll \ + --base-url \ + --session-id \ + --reply-to \ + --agent-reply +``` + +When debugging source-only Agent Session Command changes, run the same `agent +poll` subcommand through the source entrypoint from `apps/bridge` and preserve +the returned flags: + +```sh +dart run bin/ask_ui_bridge.dart agent poll \ + --base-url \ + --session-id \ + --reply-to \ + --agent-reply +``` + +For command-level workflow errors, report the problem with `--agent-error` +instead of presenting it as a normal agent reply: + +```sh +agent poll --reply-to --agent-error +``` + +Use `--agent-error` for failures such as missing local tooling, failed command +execution, invalid launch state, or a verification command that cannot be run. +Use `--agent-reply` for normal task answers, summaries, and completed work. + +After each reply or error, continue polling unless the user asks you to stop, +the session ends, or the Agent Session Command returns a workflow error that +prevents continuation. + +## Boundaries + +Do not duplicate fragile Flutter, bridge, Web, or browser orchestration details +inside this skill. The launcher owns device discovery, Flutter startup, VM +Service parsing, Bridge Server startup, Bridge Session creation, packaged Web +serving, Web development serving, browser opening, and generated Agent Session +Command arguments. + +When launch behavior needs to change, update and test the CLI contract rather +than scripting those internals here. diff --git a/skills/ask-ui/SKILL.md b/skills/ask-ui/SKILL.md new file mode 100644 index 0000000..7b632b2 --- /dev/null +++ b/skills/ask-ui/SKILL.md @@ -0,0 +1,131 @@ +--- +name: ask-ui +description: Start Ask UI from an integrated Flutter project, attach the workbench to the coding-agent session, and keep polling for UI-targeted tasks. +--- + +# Ask UI + +Use this skill when the user asks to start Ask UI, inspect a Flutter UI through +Ask UI, or continue an Ask UI Agent Session in a normal Flutter project. + +## Project Setup + +The Flutter app being inspected must depend on the Ask UI runtime: + +```sh +flutter pub add ask_ui_runtime +``` + +This should add the runtime dependency to the app's `pubspec.yaml`: + +```yaml +dependencies: + ask_ui_runtime: ^0.0.1 +``` + +The app must register the debug runtime before `runApp`: + +```dart +import 'package:ask_ui_runtime/ask_ui_runtime.dart'; + +void main() { + registerAskUiRuntime(); + runApp(const MyApp()); +} +``` + +The bridge is a globally activated development CLI: + +```sh +dart pub global activate ask_ui_bridge +``` + +## Start Ask UI + +From the Flutter project that is being inspected, run the installed bridge +executable: + +```sh +ask_ui_bridge launch +``` + +Pass Flutter launch options through the launcher when the user or project needs +a specific target: + +```sh +ask_ui_bridge launch \ + --device \ + --flavor \ + --target \ + --dart-define \ + --project-root +``` + +## Handle Launch Output + +Read the launch command JSON from stdout. Keep process logs and diagnostics out +of JSON parsing decisions unless the command exits with an error. + +If `status` is `needs_device_selection`, ask the user to choose from the +returned devices. Use the matching `suggestedCommand` for the selected device so +the launcher preserves flavor, target, Dart defines, project root, open/no-open, +and other launch intent. Do not guess when the launcher asks for device +selection. + +If `status` is `ready`, run the returned `agentCommand` exactly. This command +enters the Agent Session Command loop for the Bridge Session that the workbench +is using. + +If `status` is `error`, explain the stable launch error to the user. Retry only +when the user has fixed the underlying problem or explicitly asks you to retry. + +## Process Ask UI Messages + +Treat each message returned by `agent poll` as current-session task input. The +message may include selected widgets, comments, snapshots, or other workbench +context. Handle it like any other user request in the active coding-agent +session: inspect the project, edit files when needed, run appropriate +verification, and prepare a concise response. + +Reply through the Agent Session Command so Chat History stays synchronized with +the Ask UI workbench: + +```sh +agent poll --reply-to --agent-reply +``` + +Use the full returned command shape when it includes bridge connection flags, +for example: + +```sh +ask_ui_bridge agent poll \ + --base-url \ + --session-id \ + --reply-to \ + --agent-reply +``` + +For command-level workflow errors, report the problem with `--agent-error` +instead of presenting it as a normal agent reply: + +```sh +agent poll --reply-to --agent-error +``` + +Use `--agent-error` for failures such as missing local tooling, failed command +execution, invalid launch state, or a verification command that cannot be run. +Use `--agent-reply` for normal task answers, summaries, and completed work. + +After each reply or error, continue polling unless the user asks you to stop, +the session ends, or the Agent Session Command returns a workflow error that +prevents continuation. + +## Boundaries + +Do not duplicate fragile Flutter, bridge, Web, or browser orchestration details +inside this skill. The launcher owns device discovery, Flutter startup, VM +Service parsing, Bridge Server startup, Bridge Session creation, packaged Web +serving, browser opening, and generated Agent Session Command arguments. + +When launch behavior needs to change, update and test the CLI contract rather +than scripting those internals here.