From cb1f7179fa983350ddc22259b22fb9c2556e71f3 Mon Sep 17 00:00:00 2001 From: Divyanshu Bhargava Date: Wed, 16 Sep 2026 13:42:33 +0530 Subject: [PATCH 1/4] feat: add the stacflow SDK and move flow_ui into a pub workspace flow_ui moves unchanged to packages/flow_ui; the repo root becomes the pub workspace with melos scripts, and stacflow lives beside it as packages/stacflow so the two version in lockstep. The SDK re-exports flow_ui and adds Gemini, OpenAI and Claude adapters behind one transport seam, a client-side tool loop with read, write and destructive permissions, and the wire contract under contracts/ with its codegen. Provider call ids ride inside the tc_ tool-call ids, so tools land without a contract change. --- .claude/launch.json | 9 + .github/pull_request_template.md | 10 +- .github/workflows/ci.yml | 113 ++- .github/workflows/publish.yml | 80 +- .gitignore | 16 +- .pubignore | 39 - .vscode/launch.json | 8 +- AGENTS.md | 46 +- CLAUDE.md | 46 +- README.md | 304 +------ analysis_options.yaml | 24 +- contracts.lock | 1 + contracts/README.md | 19 + contracts/a2ui/README.md | 14 + contracts/error-codes.json | 26 + contracts/rest-api.openapi.yaml | 676 ++++++++++++++ contracts/sse-events.schema.json | 188 ++++ docs/README.md | 2 +- docs/astro.config.mjs | 4 + docs/src/content/docs/getting-started.mdx | 15 +- .../content/docs/stacflow/getting-started.mdx | 156 ++++ docs/src/content/docs/stacflow/providers.mdx | 100 +++ docs/src/content/docs/stacflow/tools.mdx | 136 +++ example/pubspec.lock | 542 ----------- .metadata => packages/flow_ui/.metadata | 0 packages/flow_ui/.pubignore | 25 + CHANGELOG.md => packages/flow_ui/CHANGELOG.md | 0 packages/flow_ui/LICENSE | 21 + packages/flow_ui/README.md | 311 +++++++ packages/flow_ui/analysis_options.yaml | 13 + .../flow_ui/assets}/chat_zero_light.png | Bin .../flow_ui/assets}/flow_ui_logo.svg | 0 .../flow_ui/example}/.gitignore | 4 +- .../flow_ui/example}/.metadata | 0 .../flow_ui/example}/README.md | 24 +- .../flow_ui/example}/analysis_options.yaml | 0 .../flow_ui/example}/android/.gitignore | 0 .../example}/android/app/build.gradle.kts | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../com/example/example/MainActivity.kt | 0 .../res/drawable-v21/launch_background.xml | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values-night/styles.xml | 0 .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../flow_ui/example}/android/build.gradle.kts | 0 .../example}/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example}/android/settings.gradle.kts | 0 .../flow_ui/example}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../example}/ios/Flutter/Debug.xcconfig | 0 .../example}/ios/Flutter/Release.xcconfig | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../example}/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../flow_ui/example}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../example}/ios/Runner/SceneDelegate.swift | 0 .../ios/RunnerTests/RunnerTests.swift | 0 packages/flow_ui/example/lib/env.dart | 5 + .../flow_ui/example}/lib/gemini_api.dart | 4 +- .../flow_ui/example}/lib/main.dart | 4 +- .../flow_ui/example}/linux/.gitignore | 0 .../flow_ui/example}/linux/CMakeLists.txt | 0 .../example}/linux/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../linux/flutter/generated_plugins.cmake | 0 .../example}/linux/runner/CMakeLists.txt | 0 .../flow_ui/example}/linux/runner/main.cc | 0 .../example}/linux/runner/my_application.cc | 0 .../example}/linux/runner/my_application.h | 0 .../flow_ui/example}/macos/.gitignore | 0 .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../Flutter/GeneratedPluginRegistrant.swift | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../example}/macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../flow_ui/example}/macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../macos/Runner/Release.entitlements | 0 .../macos/RunnerTests/RunnerTests.swift | 0 .../flow_ui/example}/pubspec.yaml | 5 +- .../flow_ui/example}/web/favicon.png | Bin .../flow_ui/example}/web/icons/Icon-192.png | Bin .../flow_ui/example}/web/icons/Icon-512.png | Bin .../example}/web/icons/Icon-maskable-192.png | Bin .../example}/web/icons/Icon-maskable-512.png | Bin .../flow_ui/example}/web/index.html | 0 .../flow_ui/example}/web/manifest.json | 0 .../flow_ui/example}/windows/.gitignore | 0 .../flow_ui/example}/windows/CMakeLists.txt | 0 .../example}/windows/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../windows/flutter/generated_plugins.cmake | 0 .../example}/windows/runner/CMakeLists.txt | 0 .../flow_ui/example}/windows/runner/Runner.rc | 0 .../windows/runner/flutter_window.cpp | 0 .../example}/windows/runner/flutter_window.h | 0 .../flow_ui/example}/windows/runner/main.cpp | 0 .../example}/windows/runner/resource.h | 0 .../windows/runner/resources/app_icon.ico | Bin .../windows/runner/runner.exe.manifest | 0 .../flow_ui/example}/windows/runner/utils.cpp | 0 .../flow_ui/example}/windows/runner/utils.h | 0 .../example}/windows/runner/win32_window.cpp | 0 .../example}/windows/runner/win32_window.h | 0 {lib => packages/flow_ui/lib}/flow_ui.dart | 0 .../lib}/src/models/flow_attachment.dart | 0 .../src/models/flow_attachment_options.dart | 0 .../lib}/src/models/flow_message_data.dart | 0 .../lib}/src/models/flow_message_part.dart | 0 .../lib}/src/styles/flow_chat_view_style.dart | 0 .../src/styles/flow_code_block_style.dart | 0 .../lib}/src/styles/flow_composer_style.dart | 0 .../src/styles/flow_confirmation_style.dart | 0 .../src/styles/flow_error_state_style.dart | 0 .../lib}/src/styles/flow_markdown_style.dart | 0 .../lib}/src/styles/flow_menu_style.dart | 0 .../styles/flow_message_actions_style.dart | 0 .../lib}/src/styles/flow_message_style.dart | 0 .../lib}/src/styles/flow_pill_style.dart | 0 .../src/styles/flow_suggestion_style.dart | 0 .../src/styles/flow_thread_list_style.dart | 0 .../lib}/src/styles/flow_toast_style.dart | 0 .../lib}/src/styles/flow_tool_style.dart | 0 .../flow_ui/lib}/src/theme/flow_colors.dart | 0 .../lib}/src/theme/flow_syntax_colors.dart | 0 .../flow_ui/lib}/src/theme/flow_theme.dart | 0 .../lib}/src/theme/flow_typography.dart | 0 .../lib}/src/utils/flow_asterisk_painter.dart | 0 .../lib}/src/utils/flow_attachment_error.dart | 0 .../src/utils/flow_attachment_intake.dart | 0 .../lib}/src/utils/flow_chip_text.dart | 0 .../lib}/src/utils/flow_circle_button.dart | 0 .../lib}/src/utils/flow_clipboard_paste.dart | 0 .../src/utils/flow_clipboard_paste_stub.dart | 0 .../src/utils/flow_clipboard_paste_web.dart | 0 .../lib}/src/utils/flow_file_drop.dart | 0 .../lib}/src/utils/flow_file_drop_stub.dart | 0 .../lib}/src/utils/flow_file_drop_web.dart | 0 .../lib}/src/utils/flow_file_picker.dart | 0 .../lib}/src/utils/flow_gradient_outline.dart | 0 .../lib}/src/utils/flow_markdown_parser.dart | 0 .../lib}/src/utils/flow_menu_core.dart | 0 .../lib}/src/utils/flow_menu_sheet.dart | 0 .../lib}/src/utils/flow_reveal_engine.dart | 0 .../lib}/src/utils/flow_selection.dart | 0 .../lib}/src/utils/flow_shimmer_sweep.dart | 0 .../src/utils/flow_syntax_highlighter.dart | 0 .../lib}/src/utils/flow_toast_layer.dart | 0 .../flow_ui/lib}/src/utils/flow_web_dom.dart | 0 .../src/widgets/flow_attachment_group.dart | 0 .../src/widgets/flow_attachment_preview.dart | 0 .../lib}/src/widgets/flow_chat_view.dart | 0 .../lib}/src/widgets/flow_code_block.dart | 0 .../lib}/src/widgets/flow_composer.dart | 0 .../lib}/src/widgets/flow_confirmation.dart | 0 .../lib}/src/widgets/flow_drop_target.dart | 0 .../lib}/src/widgets/flow_error_state.dart | 0 .../lib}/src/widgets/flow_greeting.dart | 0 .../lib}/src/widgets/flow_markdown.dart | 0 .../flow_ui/lib}/src/widgets/flow_menu.dart | 0 .../lib}/src/widgets/flow_message.dart | 0 .../src/widgets/flow_message_actions.dart | 0 .../lib}/src/widgets/flow_model_selector.dart | 0 .../flow_ui/lib}/src/widgets/flow_pill.dart | 0 .../lib}/src/widgets/flow_shimmer_text.dart | 0 .../lib}/src/widgets/flow_streaming_text.dart | 0 .../lib}/src/widgets/flow_suggestion.dart | 0 .../src/widgets/flow_thinking_indicator.dart | 0 .../flow_ui/lib}/src/widgets/flow_thread.dart | 0 .../lib}/src/widgets/flow_thread_list.dart | 0 .../flow_ui/lib}/src/widgets/flow_toast.dart | 0 .../flow_ui/lib}/src/widgets/flow_tool.dart | 0 packages/flow_ui/pubspec.yaml | 42 + packages/stacflow/.pubignore | 25 + packages/stacflow/CHANGELOG.md | 7 + packages/stacflow/LICENSE | 21 + packages/stacflow/README.md | 203 +++++ packages/stacflow/analysis_options.yaml | 5 + packages/stacflow/example/.gitignore | 51 ++ packages/stacflow/example/.metadata | 45 + packages/stacflow/example/README.md | 36 + .../stacflow/example/analysis_options.yaml | 38 + packages/stacflow/example/android/.gitignore | 14 + .../example/android/app/build.gradle.kts | 49 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 46 + .../dev/stac/stacflow_example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../stacflow/example/android/build.gradle.kts | 24 + .../example/android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle.kts | 26 + packages/stacflow/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 24 + .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 650 ++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../stacflow/example/ios/Runner/Info.plist | 70 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/Runner/SceneDelegate.swift | 6 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + packages/stacflow/example/lib/env.dart | 1 + packages/stacflow/example/lib/main.dart | 111 +++ packages/stacflow/example/linux/.gitignore | 1 + .../stacflow/example/linux/CMakeLists.txt | 128 +++ .../example/linux/flutter/CMakeLists.txt | 88 ++ .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 25 + .../example/linux/runner/CMakeLists.txt | 26 + .../stacflow/example/linux/runner/main.cc | 6 + .../example/linux/runner/my_application.cc | 148 +++ .../example/linux/runner/my_application.h | 21 + packages/stacflow/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 12 + .../macos/Runner.xcodeproj/project.pbxproj | 729 +++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 117 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 16 + .../stacflow/example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 12 + .../macos/RunnerTests/RunnerTests.swift | 12 + packages/stacflow/example/pubspec.yaml | 23 + packages/stacflow/example/web/favicon.png | Bin 0 -> 917 bytes .../stacflow/example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../stacflow/example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes packages/stacflow/example/web/index.html | 46 + packages/stacflow/example/web/manifest.json | 35 + packages/stacflow/example/windows/.gitignore | 17 + .../stacflow/example/windows/CMakeLists.txt | 108 +++ .../example/windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 14 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 25 + .../example/windows/runner/CMakeLists.txt | 40 + .../stacflow/example/windows/runner/Runner.rc | 121 +++ .../example/windows/runner/flutter_window.cpp | 71 ++ .../example/windows/runner/flutter_window.h | 33 + .../stacflow/example/windows/runner/main.cpp | 43 + .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 14 + .../stacflow/example/windows/runner/utils.cpp | 69 ++ .../stacflow/example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 288 ++++++ .../example/windows/runner/win32_window.h | 102 +++ .../stacflow/lib/src/chat/chat_state.dart | 118 +++ .../stacflow/lib/src/chat/stacflow_chat.dart | 714 +++++++++++++++ .../lib/src/chat/stacflow_chat_view.dart | 315 +++++++ .../stacflow/lib/src/chat/wire_history.dart | 139 +++ packages/stacflow/lib/src/errors.dart | 68 ++ .../stacflow/lib/src/generated/api_paths.dart | 13 + .../lib/src/generated/error_codes.dart | 169 ++++ .../lib/src/generated/rest_models.dart | 839 ++++++++++++++++++ .../lib/src/generated/sse_events.dart | 410 +++++++++ .../lib/src/providers/anthropic_provider.dart | 407 +++++++++ .../lib/src/providers/gemini_provider.dart | 376 ++++++++ .../lib/src/providers/openai_provider.dart | 385 ++++++++ .../stacflow/lib/src/providers/provider.dart | 49 + .../lib/src/providers/turn_runner.dart | 545 ++++++++++++ packages/stacflow/lib/src/strings.dart | 83 ++ packages/stacflow/lib/src/tools/tool.dart | 199 +++++ .../stacflow/lib/src/tools/tool_loop.dart | 381 ++++++++ .../lib/src/transport/sse_parser.dart | 88 ++ .../stacflow/lib/src/transport/turn_ids.dart | 87 ++ .../lib/src/transport/turn_request.dart | 115 +++ .../lib/src/transport/turn_transport.dart | 10 + packages/stacflow/lib/stacflow.dart | 24 + packages/stacflow/pubspec.yaml | 24 + packages/stacflow/tool/smoke.dart | 121 +++ playground/README.md | 2 +- playground/pubspec.yaml | 5 +- playground/pubspec.lock => pubspec.lock | 223 ++++- pubspec.yaml | 67 +- tool/README.md | 9 + tool/contracts_gen.dart | 733 +++++++++++++++ tool/contracts_hash.sh | 18 + tool/sync_contracts.sh | 21 + 397 files changed, 13821 insertions(+), 1066 deletions(-) delete mode 100644 .pubignore create mode 100644 contracts.lock create mode 100644 contracts/README.md create mode 100644 contracts/a2ui/README.md create mode 100644 contracts/error-codes.json create mode 100644 contracts/rest-api.openapi.yaml create mode 100644 contracts/sse-events.schema.json create mode 100644 docs/src/content/docs/stacflow/getting-started.mdx create mode 100644 docs/src/content/docs/stacflow/providers.mdx create mode 100644 docs/src/content/docs/stacflow/tools.mdx delete mode 100644 example/pubspec.lock rename .metadata => packages/flow_ui/.metadata (100%) create mode 100644 packages/flow_ui/.pubignore rename CHANGELOG.md => packages/flow_ui/CHANGELOG.md (100%) create mode 100644 packages/flow_ui/LICENSE create mode 100644 packages/flow_ui/README.md create mode 100644 packages/flow_ui/analysis_options.yaml rename {assets => packages/flow_ui/assets}/chat_zero_light.png (100%) rename {assets => packages/flow_ui/assets}/flow_ui_logo.svg (100%) rename {example => packages/flow_ui/example}/.gitignore (89%) rename {example => packages/flow_ui/example}/.metadata (100%) rename {example => packages/flow_ui/example}/README.md (58%) rename {example => packages/flow_ui/example}/analysis_options.yaml (100%) rename {example => packages/flow_ui/example}/android/.gitignore (100%) rename {example => packages/flow_ui/example}/android/app/build.gradle.kts (100%) rename {example => packages/flow_ui/example}/android/app/src/debug/AndroidManifest.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/main/AndroidManifest.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/main/kotlin/com/example/example/MainActivity.kt (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/drawable-v21/launch_background.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/values-night/styles.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/main/res/values/styles.xml (100%) rename {example => packages/flow_ui/example}/android/app/src/profile/AndroidManifest.xml (100%) rename {example => packages/flow_ui/example}/android/build.gradle.kts (100%) rename {example => packages/flow_ui/example}/android/gradle.properties (100%) rename {example => packages/flow_ui/example}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {example => packages/flow_ui/example}/android/settings.gradle.kts (100%) rename {example => packages/flow_ui/example}/ios/.gitignore (100%) rename {example => packages/flow_ui/example}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {example => packages/flow_ui/example}/ios/Flutter/Debug.xcconfig (100%) rename {example => packages/flow_ui/example}/ios/Flutter/Release.xcconfig (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/flow_ui/example}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => packages/flow_ui/example}/ios/Runner/AppDelegate.swift (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {example => packages/flow_ui/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {example => packages/flow_ui/example}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {example => packages/flow_ui/example}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {example => packages/flow_ui/example}/ios/Runner/Info.plist (100%) rename {example => packages/flow_ui/example}/ios/Runner/Runner-Bridging-Header.h (100%) rename {example => packages/flow_ui/example}/ios/Runner/SceneDelegate.swift (100%) rename {example => packages/flow_ui/example}/ios/RunnerTests/RunnerTests.swift (100%) create mode 100644 packages/flow_ui/example/lib/env.dart rename {example => packages/flow_ui/example}/lib/gemini_api.dart (98%) rename {example => packages/flow_ui/example}/lib/main.dart (99%) rename {example => packages/flow_ui/example}/linux/.gitignore (100%) rename {example => packages/flow_ui/example}/linux/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/linux/flutter/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/linux/flutter/generated_plugin_registrant.cc (100%) rename {example => packages/flow_ui/example}/linux/flutter/generated_plugin_registrant.h (100%) rename {example => packages/flow_ui/example}/linux/flutter/generated_plugins.cmake (100%) rename {example => packages/flow_ui/example}/linux/runner/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/linux/runner/main.cc (100%) rename {example => packages/flow_ui/example}/linux/runner/my_application.cc (100%) rename {example => packages/flow_ui/example}/linux/runner/my_application.h (100%) rename {example => packages/flow_ui/example}/macos/.gitignore (100%) rename {example => packages/flow_ui/example}/macos/Flutter/Flutter-Debug.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Flutter/Flutter-Release.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Flutter/GeneratedPluginRegistrant.swift (100%) rename {example => packages/flow_ui/example}/macos/Runner.xcodeproj/project.pbxproj (100%) rename {example => packages/flow_ui/example}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/flow_ui/example}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => packages/flow_ui/example}/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flow_ui/example}/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/flow_ui/example}/macos/Runner/AppDelegate.swift (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename {example => packages/flow_ui/example}/macos/Runner/Base.lproj/MainMenu.xib (100%) rename {example => packages/flow_ui/example}/macos/Runner/Configs/AppInfo.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Runner/Configs/Debug.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Runner/Configs/Release.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Runner/Configs/Warnings.xcconfig (100%) rename {example => packages/flow_ui/example}/macos/Runner/DebugProfile.entitlements (100%) rename {example => packages/flow_ui/example}/macos/Runner/Info.plist (100%) rename {example => packages/flow_ui/example}/macos/Runner/MainFlutterWindow.swift (100%) rename {example => packages/flow_ui/example}/macos/Runner/Release.entitlements (100%) rename {example => packages/flow_ui/example}/macos/RunnerTests/RunnerTests.swift (100%) rename {example => packages/flow_ui/example}/pubspec.yaml (89%) rename {example => packages/flow_ui/example}/web/favicon.png (100%) rename {example => packages/flow_ui/example}/web/icons/Icon-192.png (100%) rename {example => packages/flow_ui/example}/web/icons/Icon-512.png (100%) rename {example => packages/flow_ui/example}/web/icons/Icon-maskable-192.png (100%) rename {example => packages/flow_ui/example}/web/icons/Icon-maskable-512.png (100%) rename {example => packages/flow_ui/example}/web/index.html (100%) rename {example => packages/flow_ui/example}/web/manifest.json (100%) rename {example => packages/flow_ui/example}/windows/.gitignore (100%) rename {example => packages/flow_ui/example}/windows/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/windows/flutter/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/windows/flutter/generated_plugin_registrant.cc (100%) rename {example => packages/flow_ui/example}/windows/flutter/generated_plugin_registrant.h (100%) rename {example => packages/flow_ui/example}/windows/flutter/generated_plugins.cmake (100%) rename {example => packages/flow_ui/example}/windows/runner/CMakeLists.txt (100%) rename {example => packages/flow_ui/example}/windows/runner/Runner.rc (100%) rename {example => packages/flow_ui/example}/windows/runner/flutter_window.cpp (100%) rename {example => packages/flow_ui/example}/windows/runner/flutter_window.h (100%) rename {example => packages/flow_ui/example}/windows/runner/main.cpp (100%) rename {example => packages/flow_ui/example}/windows/runner/resource.h (100%) rename {example => packages/flow_ui/example}/windows/runner/resources/app_icon.ico (100%) rename {example => packages/flow_ui/example}/windows/runner/runner.exe.manifest (100%) rename {example => packages/flow_ui/example}/windows/runner/utils.cpp (100%) rename {example => packages/flow_ui/example}/windows/runner/utils.h (100%) rename {example => packages/flow_ui/example}/windows/runner/win32_window.cpp (100%) rename {example => packages/flow_ui/example}/windows/runner/win32_window.h (100%) rename {lib => packages/flow_ui/lib}/flow_ui.dart (100%) rename {lib => packages/flow_ui/lib}/src/models/flow_attachment.dart (100%) rename {lib => packages/flow_ui/lib}/src/models/flow_attachment_options.dart (100%) rename {lib => packages/flow_ui/lib}/src/models/flow_message_data.dart (100%) rename {lib => packages/flow_ui/lib}/src/models/flow_message_part.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_chat_view_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_code_block_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_composer_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_confirmation_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_error_state_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_markdown_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_menu_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_message_actions_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_message_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_pill_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_suggestion_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_thread_list_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_toast_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/styles/flow_tool_style.dart (100%) rename {lib => packages/flow_ui/lib}/src/theme/flow_colors.dart (100%) rename {lib => packages/flow_ui/lib}/src/theme/flow_syntax_colors.dart (100%) rename {lib => packages/flow_ui/lib}/src/theme/flow_theme.dart (100%) rename {lib => packages/flow_ui/lib}/src/theme/flow_typography.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_asterisk_painter.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_attachment_error.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_attachment_intake.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_chip_text.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_circle_button.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_clipboard_paste.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_clipboard_paste_stub.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_clipboard_paste_web.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_file_drop.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_file_drop_stub.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_file_drop_web.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_file_picker.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_gradient_outline.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_markdown_parser.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_menu_core.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_menu_sheet.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_reveal_engine.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_selection.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_shimmer_sweep.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_syntax_highlighter.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_toast_layer.dart (100%) rename {lib => packages/flow_ui/lib}/src/utils/flow_web_dom.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_attachment_group.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_attachment_preview.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_chat_view.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_code_block.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_composer.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_confirmation.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_drop_target.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_error_state.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_greeting.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_markdown.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_menu.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_message.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_message_actions.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_model_selector.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_pill.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_shimmer_text.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_streaming_text.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_suggestion.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_thinking_indicator.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_thread.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_thread_list.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_toast.dart (100%) rename {lib => packages/flow_ui/lib}/src/widgets/flow_tool.dart (100%) create mode 100644 packages/flow_ui/pubspec.yaml create mode 100644 packages/stacflow/.pubignore create mode 100644 packages/stacflow/CHANGELOG.md create mode 100644 packages/stacflow/LICENSE create mode 100644 packages/stacflow/README.md create mode 100644 packages/stacflow/analysis_options.yaml create mode 100644 packages/stacflow/example/.gitignore create mode 100644 packages/stacflow/example/.metadata create mode 100644 packages/stacflow/example/README.md create mode 100644 packages/stacflow/example/analysis_options.yaml create mode 100644 packages/stacflow/example/android/.gitignore create mode 100644 packages/stacflow/example/android/app/build.gradle.kts create mode 100644 packages/stacflow/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/stacflow/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/stacflow/example/android/app/src/main/kotlin/dev/stac/stacflow_example/MainActivity.kt create mode 100644 packages/stacflow/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/stacflow/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/stacflow/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/stacflow/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/stacflow/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/stacflow/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/stacflow/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/stacflow/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/stacflow/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/stacflow/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/stacflow/example/android/build.gradle.kts create mode 100644 packages/stacflow/example/android/gradle.properties create mode 100644 packages/stacflow/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/stacflow/example/android/settings.gradle.kts create mode 100644 packages/stacflow/example/ios/.gitignore create mode 100644 packages/stacflow/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/stacflow/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/stacflow/example/ios/Flutter/Release.xcconfig create mode 100644 packages/stacflow/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/stacflow/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/stacflow/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stacflow/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/stacflow/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/stacflow/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/stacflow/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stacflow/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/stacflow/example/ios/Runner/AppDelegate.swift create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/stacflow/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/stacflow/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/stacflow/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/stacflow/example/ios/Runner/Info.plist create mode 100644 packages/stacflow/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packages/stacflow/example/ios/Runner/SceneDelegate.swift create mode 100644 packages/stacflow/example/ios/RunnerTests/RunnerTests.swift create mode 100644 packages/stacflow/example/lib/env.dart create mode 100644 packages/stacflow/example/lib/main.dart create mode 100644 packages/stacflow/example/linux/.gitignore create mode 100644 packages/stacflow/example/linux/CMakeLists.txt create mode 100644 packages/stacflow/example/linux/flutter/CMakeLists.txt create mode 100644 packages/stacflow/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/stacflow/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/stacflow/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/stacflow/example/linux/runner/CMakeLists.txt create mode 100644 packages/stacflow/example/linux/runner/main.cc create mode 100644 packages/stacflow/example/linux/runner/my_application.cc create mode 100644 packages/stacflow/example/linux/runner/my_application.h create mode 100644 packages/stacflow/example/macos/.gitignore create mode 100644 packages/stacflow/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/stacflow/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/stacflow/example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 packages/stacflow/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/stacflow/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stacflow/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/stacflow/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/stacflow/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stacflow/example/macos/Runner/AppDelegate.swift create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/stacflow/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 packages/stacflow/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/stacflow/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/stacflow/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/stacflow/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/stacflow/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/stacflow/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/stacflow/example/macos/Runner/Info.plist create mode 100644 packages/stacflow/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/stacflow/example/macos/Runner/Release.entitlements create mode 100644 packages/stacflow/example/macos/RunnerTests/RunnerTests.swift create mode 100644 packages/stacflow/example/pubspec.yaml create mode 100644 packages/stacflow/example/web/favicon.png create mode 100644 packages/stacflow/example/web/icons/Icon-192.png create mode 100644 packages/stacflow/example/web/icons/Icon-512.png create mode 100644 packages/stacflow/example/web/icons/Icon-maskable-192.png create mode 100644 packages/stacflow/example/web/icons/Icon-maskable-512.png create mode 100644 packages/stacflow/example/web/index.html create mode 100644 packages/stacflow/example/web/manifest.json create mode 100644 packages/stacflow/example/windows/.gitignore create mode 100644 packages/stacflow/example/windows/CMakeLists.txt create mode 100644 packages/stacflow/example/windows/flutter/CMakeLists.txt create mode 100644 packages/stacflow/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 packages/stacflow/example/windows/flutter/generated_plugin_registrant.h create mode 100644 packages/stacflow/example/windows/flutter/generated_plugins.cmake create mode 100644 packages/stacflow/example/windows/runner/CMakeLists.txt create mode 100644 packages/stacflow/example/windows/runner/Runner.rc create mode 100644 packages/stacflow/example/windows/runner/flutter_window.cpp create mode 100644 packages/stacflow/example/windows/runner/flutter_window.h create mode 100644 packages/stacflow/example/windows/runner/main.cpp create mode 100644 packages/stacflow/example/windows/runner/resource.h create mode 100644 packages/stacflow/example/windows/runner/resources/app_icon.ico create mode 100644 packages/stacflow/example/windows/runner/runner.exe.manifest create mode 100644 packages/stacflow/example/windows/runner/utils.cpp create mode 100644 packages/stacflow/example/windows/runner/utils.h create mode 100644 packages/stacflow/example/windows/runner/win32_window.cpp create mode 100644 packages/stacflow/example/windows/runner/win32_window.h create mode 100644 packages/stacflow/lib/src/chat/chat_state.dart create mode 100644 packages/stacflow/lib/src/chat/stacflow_chat.dart create mode 100644 packages/stacflow/lib/src/chat/stacflow_chat_view.dart create mode 100644 packages/stacflow/lib/src/chat/wire_history.dart create mode 100644 packages/stacflow/lib/src/errors.dart create mode 100644 packages/stacflow/lib/src/generated/api_paths.dart create mode 100644 packages/stacflow/lib/src/generated/error_codes.dart create mode 100644 packages/stacflow/lib/src/generated/rest_models.dart create mode 100644 packages/stacflow/lib/src/generated/sse_events.dart create mode 100644 packages/stacflow/lib/src/providers/anthropic_provider.dart create mode 100644 packages/stacflow/lib/src/providers/gemini_provider.dart create mode 100644 packages/stacflow/lib/src/providers/openai_provider.dart create mode 100644 packages/stacflow/lib/src/providers/provider.dart create mode 100644 packages/stacflow/lib/src/providers/turn_runner.dart create mode 100644 packages/stacflow/lib/src/strings.dart create mode 100644 packages/stacflow/lib/src/tools/tool.dart create mode 100644 packages/stacflow/lib/src/tools/tool_loop.dart create mode 100644 packages/stacflow/lib/src/transport/sse_parser.dart create mode 100644 packages/stacflow/lib/src/transport/turn_ids.dart create mode 100644 packages/stacflow/lib/src/transport/turn_request.dart create mode 100644 packages/stacflow/lib/src/transport/turn_transport.dart create mode 100644 packages/stacflow/lib/stacflow.dart create mode 100644 packages/stacflow/pubspec.yaml create mode 100644 packages/stacflow/tool/smoke.dart rename playground/pubspec.lock => pubspec.lock (74%) create mode 100644 tool/README.md create mode 100644 tool/contracts_gen.dart create mode 100755 tool/contracts_hash.sh create mode 100755 tool/sync_contracts.sh diff --git a/.claude/launch.json b/.claude/launch.json index 606ffb2..bdeb9cc 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -15,6 +15,15 @@ "cd playground && flutter run -d web-server --web-port=8123" ], "port": 8123 + }, + { + "name": "stacflow-example", + "runtimeExecutable": "sh", + "runtimeArgs": [ + "-c", + "cd packages/stacflow/example && flutter run -d web-server --web-port=8124 $([ -f env.json ] && echo --dart-define-from-file=env.json)" + ], + "port": 8124 } ] } diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fede0cc..d45c429 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,11 +18,11 @@ ## Checklist -- [ ] `flutter analyze lib` and `flutter analyze` in `example/` and `playground/` are clean -- [ ] `dart format .` applied +- [ ] `dart run melos run analyze` is clean +- [ ] `dart run melos run format` applied - [ ] Exercised in the playground — with a stage demo added or updated if this is a new component or variant -- [ ] Any new entry under `dependencies:` in `pubspec.yaml` is flutter.dev-published, forces no configuration on hosts that never use the feature, and is argued in this PR +- [ ] Any new entry under `dependencies:` in `packages/flow_ui/pubspec.yaml` is flutter.dev-published, forces no configuration on hosts that never use the feature, and is argued in this PR - [ ] Nothing model-facing — no prompts, schemas, or provider/network calls -- [ ] New public API is exported from `lib/flow_ui.dart` and documented in `docs/` and the README table -- [ ] `CHANGELOG.md` updated for user-facing changes, with breaking changes called out +- [ ] New public API is exported from `packages/flow_ui/lib/flow_ui.dart` and documented in `docs/` and the README table +- [ ] `packages/flow_ui/CHANGELOG.md` updated for user-facing changes, with breaking changes called out - [ ] PR title follows conventional commits (`feat:`, `fix:`, `refactor:`, `docs:`, `chore:`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82f33c7..42e1104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: pull_request: workflow_dispatch: -# A new push to the same ref supersedes the run already in flight. concurrency: group: ci-${{ github.ref }} cancel-in-progress: true @@ -15,8 +14,8 @@ permissions: contents: read jobs: - package: - name: Package + workspace: + name: Workspace runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -31,57 +30,62 @@ jobs: - name: Format run: dart format --output=none --set-exit-if-changed . - # Scoped to lib/: from the repo root the analyzer also walks into - # example/ and playground/, which resolve their own dependencies and - # are analysed in the `apps` job instead. - name: Analyze - run: flutter analyze lib + run: dart run melos run analyze - # test/ is deliberately empty while the component surface is still being - # reshaped design-first; this runs the day the first test lands. - name: Test - run: | - if find test -name '*_test.dart' 2>/dev/null | grep -q .; then - flutter test - else - echo "No tests yet — skipping." - fi - - # Catches packaging mistakes (a stale .pubignore, a missing asset, a - # broken README link) before a release tag, not after. + run: dart run melos run test + + package: + name: flow_ui package + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/flow_ui + steps: + - uses: actions/checkout@v7 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - run: flutter pub get + + - run: flutter analyze + - name: Publish dry run run: flutter pub publish --dry-run apps: - name: ${{ matrix.app }} + name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - app: [example, playground] + include: + - name: example + dir: packages/flow_ui/example + - name: stacflow example + dir: packages/stacflow/example + - name: playground + dir: playground steps: - uses: actions/checkout@v7 - # The example reads its Gemini key from lib/env.g.dart, which is - # gitignored so a real key can never land in a commit — meaning a - # fresh checkout doesn't have it. Stub it with an empty key, the - # same state a fresh clone runs with (see example/README.md). - - name: Stub the example's key file - if: matrix.app == 'example' - run: | - printf '// CI stub — the real file is gitignored.\nconst String apiKey = %s;\n' "''" \ - > example/lib/env.g.dart - - uses: subosito/flutter-action@v2 with: channel: stable cache: true - run: flutter pub get - working-directory: ${{ matrix.app }} + working-directory: ${{ matrix.dir }} - run: flutter analyze - working-directory: ${{ matrix.app }} + working-directory: ${{ matrix.dir }} + + - run: flutter build web --release + working-directory: ${{ matrix.dir }} docs: name: Docs site @@ -89,8 +93,6 @@ jobs: steps: - uses: actions/checkout@v7 - # `build:site` compiles the playground to wasm before Astro builds, so - # this job needs both toolchains. - uses: subosito/flutter-action@v2 with: channel: stable @@ -105,7 +107,46 @@ jobs: - run: npm ci working-directory: docs - # The same build the deploy runs: playground → public/playground, then - # the site. Keeps a broken playground from reaching flowui.stac.dev. - run: npm run build:site working-directory: docs + + # Dependency-direction rules the analyzer cannot express. Greps anchor to + # import/export directives so prose in doc comments never trips them. + boundaries: + name: Boundaries + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: flow_ui never depends on the SDK package + run: | + ! grep -rnE "^\s*(import|export)\s+'package:stacflow/" packages/flow_ui/lib \ + || { echo "::error::flow_ui is the presentation layer; the SDK depends on it, never the reverse"; exit 1; } + ! grep -nE "^\s*stacflow:" packages/flow_ui/pubspec.yaml \ + || { echo "::error::flow_ui must not depend on the SDK package"; exit 1; } + + contracts: + name: Contracts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - run: flutter pub get + + - name: Lock matches contracts/ + run: | + test "$(./tool/contracts_hash.sh)" = "$(cat contracts.lock)" \ + || { echo "::error::contracts/ changed without updating contracts.lock: run ./tool/contracts_hash.sh > contracts.lock"; exit 1; } + + - name: Generated Dart matches contracts/ + run: | + dart run tool/contracts_gen.dart + dart format packages/stacflow/lib/src/generated >/dev/null + git add -N packages/stacflow/lib/src/generated + git diff --exit-code -- packages/stacflow/lib/src/generated \ + || { echo "::error::generated types are stale: run dart run tool/contracts_gen.dart && dart format packages/stacflow/lib/src/generated"; exit 1; } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index da0b86f..ac32566 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,33 +1,62 @@ -# One release trigger, everything ships: pushing a version tag publishes -# the package to pub.dev AND deploys the docs site — playground included, -# it's built into dist/playground/ — to Cloudflare Pages. +# One tag per package release: # -# git tag v0.2.0 && git push origin v0.2.0 +# git tag flow_ui-v0.4.0 && git push origin flow_ui-v0.4.0 # -# Package authentication is OIDC — no token lives in this repo. It requires -# automated publishing to be enabled once on pub.dev (flow_ui → Admin → -# Automated publishing) for StacDev/flow_ui with the tag pattern -# `v{{version}}`. +# publishes packages/flow_ui to pub.dev and deploys the docs site (playground +# included) to Cloudflare Pages. Publishing is OIDC: pub.dev → flow_ui → Admin +# → Automated publishing must list StacDev/flow_ui with the tag pattern +# `flow_ui-v{{version}}`. A future package gets its own tag glob below, a +# `case` line in `meta`, and its own pub.dev tag pattern. # -# The site deploy needs one-time repo configuration: -# secrets: CLOUDFLARE_API_TOKEN (Pages:Edit permission) -# CLOUDFLARE_ACCOUNT_ID -# variable: CLOUDFLARE_PAGES_PROJECT (the Pages project serving -# flowui.stac.dev) +# Site deploy configuration: secrets CLOUDFLARE_API_TOKEN (Pages:Edit) and +# CLOUDFLARE_ACCOUNT_ID; variable CLOUDFLARE_PAGES_PROJECT. name: Publish on: push: - tags: ['v[0-9]+.[0-9]+.[0-9]+*'] - # Redeploy the site without cutting a release, e.g. after a docs fix. + tags: + - 'flow_ui-v[0-9]+.[0-9]+.[0-9]+*' + - 'stacflow-v[0-9]+.[0-9]+.[0-9]+*' + # Redeploy the site without cutting a release. workflow_dispatch: jobs: - # The quality gate: the release only ships with full pub.dev points. - # Comparing granted against pana's own max (160 today) keeps the gate - # honest if pub.dev ever raises the ceiling. + meta: + name: Resolve package from tag + runs-on: ubuntu-latest + outputs: + package: ${{ steps.tag.outputs.package }} + dir: ${{ steps.tag.outputs.dir }} + version: ${{ steps.tag.outputs.version }} + steps: + - uses: actions/checkout@v7 + + - id: tag + shell: bash + run: | + if [ "$GITHUB_EVENT_NAME" != push ]; then + printf 'package=flow_ui\ndir=packages/flow_ui\nversion=\n' >> "$GITHUB_OUTPUT" + exit 0 + fi + tag="$GITHUB_REF_NAME" + package="${tag%-v*}" + version="${tag#"$package-v"}" + case "$package" in + flow_ui) dir=packages/flow_ui ;; + stacflow) dir=packages/stacflow ;; + *) echo "::error::no package for tag $tag"; exit 1 ;; + esac + pubspec_version=$(sed -n 's/^version:[[:space:]]*//p' "$dir/pubspec.yaml" | tr -d '[:space:]') + if [ "$version" != "$pubspec_version" ]; then + echo "::error::tag says $version, $dir/pubspec.yaml says $pubspec_version" + exit 1 + fi + printf 'package=%s\ndir=%s\nversion=%s\n' "$package" "$dir" "$version" >> "$GITHUB_OUTPUT" + + # The release only ships with full pub.dev points. pana: name: Pana score gate + needs: meta runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -42,11 +71,11 @@ jobs: - name: Require a perfect score run: | dart pub global activate pana - dart pub global run pana --no-warning --json . > pana.json + dart pub global run pana --no-warning --json "${{ needs.meta.outputs.dir }}" > pana.json granted=$(jq -r '.scores.grantedPoints' pana.json) max=$(jq -r '.scores.maxPoints' pana.json) echo "pana: $granted / $max" - if [ "$granted" != "$max" ]; then + if [ "$granted" -lt "$max" ]; then echo "Points were dropped in these sections:" jq -r '.report.sections[] | select(.grantedPoints < .maxPoints) @@ -56,22 +85,21 @@ jobs: fi package: - # Manual dispatches redeploy the site only — pub.dev is tags-only. if: github.event_name == 'push' - needs: pana + needs: [meta, pana] permissions: - id-token: write # Required to mint the OIDC token pub.dev verifies. + id-token: write uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + with: + working-directory: ${{ needs.meta.outputs.dir }} site: name: Docs + playground - needs: pana + needs: [meta, pana] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - # `build:site` compiles the playground to wasm before Astro builds, - # so this job needs both toolchains — the same build CI validates. - uses: subosito/flutter-action@v2 with: channel: stable diff --git a/.gitignore b/.gitignore index 1e0c933..bff98da 100644 --- a/.gitignore +++ b/.gitignore @@ -16,17 +16,13 @@ migrate_working_dir/ *.iws .idea/ .env +.codex/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock +# Flutter/Dart/Pub related. The root pubspec.lock is committed: the +# workspace holds apps. **/doc/api/ .dart_tool/ .flutter-plugins-dependencies -/build/ -/coverage/ +build/ +coverage/ +pubspec_overrides.yaml diff --git a/.pubignore b/.pubignore deleted file mode 100644 index 385b939..0000000 --- a/.pubignore +++ /dev/null @@ -1,39 +0,0 @@ -# .pubignore replaces .gitignore for publishing, so it repeats the relevant -# rules and adds the repo-only files the package archive should not carry. - -# From .gitignore -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ -*.iml -*.ipr -*.iws -.idea/ -.env -# Unrooted (unlike .gitignore) so example/ stays covered even when -# publishing from outside a git checkout, where .gitignore files are not -# consulted. -pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins-dependencies -build/ -/coverage/ - -# Repo-only: the playground app, the documentation site, README screenshots -# (pub.dev loads them from the repository), and development/agent docs -playground/ -docs/ -doc/ -AGENTS.md -CLAUDE.md -.claude/ -.vscode/ -.metadata diff --git a/.vscode/launch.json b/.vscode/launch.json index 8edc41a..c612b35 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,13 @@ }, { "name": "example", - "cwd": "example", + "cwd": "packages/flow_ui/example", + "request": "launch", + "type": "dart" + }, + { + "name": "stacflow example", + "cwd": "packages/stacflow/example", "request": "launch", "type": "dart" } diff --git a/AGENTS.md b/AGENTS.md index a2dc0e8..84c9070 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t ## What this is -**flow_ui** is a chat/assistant UI component library for Flutter — the presentation layer for AI assistant interfaces. It is a plain Flutter package (no codegen, no melos). Downstream packages depend on the public API exported from `lib/flow_ui.dart`, so treat it as a compatibility surface. +**flow_ui** is a chat/assistant UI component library for Flutter — the presentation layer for AI assistant interfaces. It is a plain Flutter package (no codegen) at `packages/flow_ui/`, inside a pub workspace driven by melos. Downstream packages depend on the public API exported from `lib/flow_ui.dart`, so treat it as a compatibility surface. Two hard constraints shape everything here: @@ -15,19 +15,24 @@ The theme, the conversation components (message, thread, streaming text, actions ## Layout -- Package at the repo root: `lib/`, `test/`, `pubspec.yaml`. -- `playground/` — the Flow UI Playground: a full Flutter app depending on the package via `path: ../`. Use it to demo and manually exercise components (every component has a stage demo, with variant pills and code snippets). +- Root `pubspec.yaml` is the pub workspace (members under `workspace:`) with the melos scripts; root `analysis_options.yaml` (very_good_analysis) governs `tool/` and the SDK package only. +- `packages/flow_ui/` — the published package: `lib/`, `example/` (the README's chat screen against Gemini), `assets/`, its own flutter_lints `analysis_options.yaml` and `.pubignore`. +- `packages/stacflow/` — the StacFlow SDK package (see "SDK package"), with `example/` (the README's chat screen against Gemini; flutter_lints like the flow_ui example). +- `playground/` — the Flow UI Playground: a full Flutter app and workspace member depending on `flow_ui: ^0.4.0`. Use it to demo and manually exercise components (every component has a stage demo, with variant pills and code snippets). +- `docs/` — the Astro site behind flowui.stac.dev. `contracts/`, `contracts.lock` and `tool/` — the SDK wire contract and its codegen. ## Commands +**Don't write comments unless asked.** No doc comments, file headers or inline explanations in new or edited code; the code and the commit message carry the intent. The one exception is a comment a lint requires (for example `document_ignores` above an `// ignore`), kept to one line. Public API dartdoc is written only when the user asks for it. + From the repo root: ```bash -flutter test # all tests -flutter test test/some_widget_test.dart # single file -flutter test --plain-name "some test name" # single test by name -flutter analyze -dart format . +flutter pub get # resolves the whole workspace +dart run melos run analyze # dart analyze --fatal-infos in every member +dart run melos run format +dart run melos run test # SDK package tests; flow_ui has none yet +cd packages/flow_ui && flutter analyze && flutter pub publish --dry-run ``` Playground app: @@ -38,6 +43,31 @@ flutter pub get flutter run -d chrome # or any device ``` +## SDK package + +`packages/stacflow` is the StacFlow SDK: `StacFlowChat` (the controller) and `StacFlowChatView` on flow_ui, wired to Gemini, OpenAI and Claude with the developer's own key. One entrypoint, `package:stacflow/stacflow.dart`, which also re-exports flow_ui. Layout: `src/chat` (controller, state, view, and the flow_ui-to-wire reduction in `wire_history.dart`), `src/transport` (the `TurnTransport` seam, `TurnRequest` and the wire types, ids, the SSE parser; pure Dart), `src/providers` (the interface, the shared HTTP runner, one adapter per provider; pure Dart), `src/tools` (`Tool`, the call records and `runToolLoop`, the client-side tool loop shared by the controller and the smoke script; pure Dart). Rules: + +- `stacflow` depends on `flow_ui`, never the reverse (CI grep). +- Adapters emit the generated `SseEvent` union: `start` first, one `done` last, `seq` from 0. The runner in `turn_runner.dart` owns HTTP, abort, timeouts, key scrubbing and the tool-call bookkeeping (ids, argument buffering, the upgrade of `complete()` to `done{awaiting_client_tools}`); an adapter only declares `TurnRequest.tools`, maps frames, encodes the wire tool parts in history and replays its own raw content within a turn where the provider requires it (Gemini signatures and ids, Claude thinking blocks). +- The loop in `tool_loop.dart` owns dispatch, approval, timeouts, abort and the continuation segments; the controller only renders blocks into parts, keeps `ChatState.toolCalls`, and answers confirmations. `contracts/` is unchanged by tools: provider call ids ride inside the `tc_` ids. +- The API key is a private field set on exactly one header, and never appears in URLs, logs, `toString` or error text. +- No tests for now. Verify with `dart analyze --fatal-infos`, the smoke script (`cd packages/stacflow && dart run --define=PROVIDER=gemini --define=GEMINI_API_KEY=... tool/smoke.dart`, also `anthropic` and `openai`; `--define=ABORT_AFTER_FIRST_DELTA=true` and `--define=IMAGE=path.png` exercise abort and image input; `--define=TOOLS=true` registers a `get_time` tool and runs the loop, with `TOOL_PERMISSION=destructive` and `DECLINE=true` for the approval paths), and the example app (`cd packages/stacflow/example && flutter run --dart-define-from-file=env.json` with `GEMINI_API_KEY` in the gitignored `env.json`; the `stacflow-example` entry in `.claude/launch.json` serves it on port 8124). Keep the example the runnable form of the README quickstart, against Gemini only, with the `set_theme` tool as its one tool. + +`contracts/` is the wire contract (SSE events, REST surface, error codes), mirrored into the private stacflow-cloud repo; `contracts.lock` is its hash and CI fails when they disagree. After editing anything under `contracts/`: + +```bash +dart run tool/contracts_gen.dart # regenerates packages/stacflow/lib/src/generated +dart format packages/stacflow/lib/src/generated +./tool/contracts_hash.sh > contracts.lock +./tool/sync_contracts.sh # mirrors into ../stacflow-cloud; commit there separately +``` + +Never hand-edit the generated directory: the analyzer excludes it and CI regenerates it. + +## Releases + +Per-package tags: `flow_ui-v` publishes `packages/flow_ui` and deploys the docs site (`.github/workflows/publish.yml`). Before tagging, bump `version:` in `packages/flow_ui/pubspec.yaml`, the `flow_ui:` constraint in `packages/flow_ui/example`, `playground` and `packages/stacflow` (pre-1.0 caret ranges), and `CHANGELOG.md`. `stacflow-v` publishes `packages/stacflow` the same way; before tagging, bump `version:` in `packages/stacflow/pubspec.yaml`, the `stacflow:` constraint in `packages/stacflow/example`, and its `CHANGELOG.md`. The pana gate wants a perfect score from both packages. + ## Commits Commit messages are conventional commits with a brief summary line: diff --git a/CLAUDE.md b/CLAUDE.md index 19940f1..3eb8f96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -**flow_ui** is a chat/assistant UI component library for Flutter — the presentation layer for AI assistant interfaces. It is a plain Flutter package (no codegen, no melos). Downstream packages depend on the public API exported from `lib/flow_ui.dart`, so treat it as a compatibility surface. +**flow_ui** is a chat/assistant UI component library for Flutter — the presentation layer for AI assistant interfaces. It is a plain Flutter package (no codegen) at `packages/flow_ui/`, inside a pub workspace driven by melos. Downstream packages depend on the public API exported from `lib/flow_ui.dart`, so treat it as a compatibility surface. Two hard constraints shape everything here: @@ -15,19 +15,26 @@ The theme, the conversation components (message, thread, streaming text, actions ## Layout -- Package at the repo root: `lib/`, `test/`, `pubspec.yaml`. -- `playground/` — the Flow UI Playground: a full Flutter app depending on the package via `path: ../`. Use it to demo and manually exercise components (every component has a stage demo, with variant pills and code snippets). +- Root `pubspec.yaml` is the pub workspace (members under `workspace:`) with the melos scripts; root `analysis_options.yaml` (very_good_analysis) governs `tool/` and the SDK package only. +- `packages/flow_ui/` — the published package: `lib/`, `example/` (the README's chat screen against Gemini), `assets/`, its own flutter_lints `analysis_options.yaml` and `.pubignore`. +- `packages/stacflow/` — the StacFlow SDK package (see "SDK package"), with `example/` (the README's chat screen against Gemini; flutter_lints like the flow_ui example). +- `playground/` — the Flow UI Playground: a full Flutter app and workspace member depending on `flow_ui: ^0.4.0`. Use it to demo and manually exercise components (every component has a stage demo, with variant pills and code snippets). +- `docs/` — the Astro site behind flowui.stac.dev. `contracts/`, `contracts.lock` and `tool/` — the SDK wire contract and its codegen. ## Commands -**Don't write tests for now.** `test/` is deliberately empty — the component surface is still being reshaped design-first, so tests written now would mostly encode values about to change. Verify a change with `flutter analyze` and by exercising it in the `playground/` app, not by adding a test file. If something seems to genuinely need one, say so and let the user decide. +**Don't write tests for now.** `packages/flow_ui` has no `test/` yet: the component surface is still being reshaped design-first, so tests written now would mostly encode values about to change. Verify a change with `flutter analyze` and by exercising it in the `playground/` app, not by adding a test file. If something seems to genuinely need one, say so and let the user decide. + +**Don't write comments unless asked.** No doc comments, file headers or inline explanations in new or edited code; the code and the commit message carry the intent. The one exception is a comment a lint requires (for example `document_ignores` above an `// ignore`), kept to one line. Public API dartdoc is written only when the user asks for it. From the repo root: ```bash -flutter analyze -dart format . -flutter test # all tests (see above — none yet) +flutter pub get # resolves the whole workspace +dart run melos run analyze # dart analyze --fatal-infos in every member +dart run melos run format +dart run melos run test # SDK package tests; flow_ui has none yet +cd packages/flow_ui && flutter analyze && flutter pub publish --dry-run ``` Playground app: @@ -38,6 +45,31 @@ flutter pub get flutter run -d chrome # or any device ``` +## SDK package + +`packages/stacflow` is the StacFlow SDK: `StacFlowChat` (the controller) and `StacFlowChatView` on flow_ui, wired to Gemini, OpenAI and Claude with the developer's own key. One entrypoint, `package:stacflow/stacflow.dart`, which also re-exports flow_ui. Layout: `src/chat` (controller, state, view, and the flow_ui-to-wire reduction in `wire_history.dart`), `src/transport` (the `TurnTransport` seam, `TurnRequest` and the wire types, ids, the SSE parser; pure Dart), `src/providers` (the interface, the shared HTTP runner, one adapter per provider; pure Dart), `src/tools` (`Tool`, the call records and `runToolLoop`, the client-side tool loop shared by the controller and the smoke script; pure Dart). Rules: + +- `stacflow` depends on `flow_ui`, never the reverse (CI grep). +- Adapters emit the generated `SseEvent` union: `start` first, one `done` last, `seq` from 0. The runner in `turn_runner.dart` owns HTTP, abort, timeouts, key scrubbing and the tool-call bookkeeping (ids, argument buffering, the upgrade of `complete()` to `done{awaiting_client_tools}`); an adapter only declares `TurnRequest.tools`, maps frames, encodes the wire tool parts in history and replays its own raw content within a turn where the provider requires it (Gemini signatures and ids, Claude thinking blocks). +- The loop in `tool_loop.dart` owns dispatch, approval, timeouts, abort and the continuation segments; the controller only renders blocks into parts, keeps `ChatState.toolCalls`, and answers confirmations. `contracts/` is unchanged by tools: provider call ids ride inside the `tc_` ids. +- The API key is a private field set on exactly one header, and never appears in URLs, logs, `toString` or error text. +- No tests for now. Verify with `dart analyze --fatal-infos`, the smoke script (`cd packages/stacflow && dart run --define=PROVIDER=gemini --define=GEMINI_API_KEY=... tool/smoke.dart`, also `anthropic` and `openai`; `--define=ABORT_AFTER_FIRST_DELTA=true` and `--define=IMAGE=path.png` exercise abort and image input; `--define=TOOLS=true` registers a `get_time` tool and runs the loop, with `TOOL_PERMISSION=destructive` and `DECLINE=true` for the approval paths), and the example app (`cd packages/stacflow/example && flutter run --dart-define-from-file=env.json` with `GEMINI_API_KEY` in the gitignored `env.json`; the `stacflow-example` entry in `.claude/launch.json` serves it on port 8124). Keep the example the runnable form of the README quickstart, against Gemini only, with the `set_theme` tool as its one tool. + +`contracts/` is the wire contract (SSE events, REST surface, error codes), mirrored into the private stacflow-cloud repo; `contracts.lock` is its hash and CI fails when they disagree. After editing anything under `contracts/`: + +```bash +dart run tool/contracts_gen.dart # regenerates packages/stacflow/lib/src/generated +dart format packages/stacflow/lib/src/generated +./tool/contracts_hash.sh > contracts.lock +./tool/sync_contracts.sh # mirrors into ../stacflow-cloud; commit there separately +``` + +Never hand-edit the generated directory: the analyzer excludes it and CI regenerates it. + +## Releases + +Per-package tags: `flow_ui-v` publishes `packages/flow_ui` and deploys the docs site (`.github/workflows/publish.yml`). Before tagging, bump `version:` in `packages/flow_ui/pubspec.yaml`, the `flow_ui:` constraint in `packages/flow_ui/example`, `playground` and `packages/stacflow` (pre-1.0 caret ranges), and `CHANGELOG.md`. `stacflow-v` publishes `packages/stacflow` the same way; before tagging, bump `version:` in `packages/stacflow/pubspec.yaml`, the `stacflow:` constraint in `packages/stacflow/example`, and its `CHANGELOG.md`. The pana gate wants a perfect score from both packages. + ## Commits Commit messages are conventional commits with a brief summary line: diff --git a/README.md b/README.md index 017e687..29a59dd 100644 --- a/README.md +++ b/README.md @@ -1,311 +1,37 @@

- Flow UI logo + Flow UI logo

flow_ui

-

- pub package - pub points - github stars - license: MIT -

-

📚 Documentation · 🧩 Playground - · 🤓 API Reference - · 🗺️ Roadmap + · 📦 pub.dev

-Flow UI is an open-source Flutter UI library to build production-grade Chat & AI assistant interfaces. +Flutter UI library for AI chat interfaces, and the StacFlow SDK built on it.

-![The flow_ui chat surface](assets/chat_zero_light.png) - -> [!IMPORTANT] -> flow_ui is pre-1.0. The API is still settling, and minor releases may -> carry breaking changes — pin a minor version and read the -> [changelog](CHANGELOG.md) when upgrading. - -## What's in the box - -| Component | What it does | -|---|---| -| [`FlowChatView`](https://flowui.stac.dev/components/chat-view) | The full chat surface: bounded thread over a composer, centred at a readable width, with a zero state (greeting, lifted composer, starters), a jump-to-latest button, and drag-and-drop with a washed invitation overlay (web) | -| [`FlowThread`](https://flowui.stac.dev/components/message-thread) | Scrollable conversation: reads from the top, anchoring to the newest message once it outgrows the viewport; text selects across turns and copies with its line breaks | -| [`FlowThreadList`](https://flowui.stac.dev/components/thread-list) | Conversation history for a side panel — host-labeled sections, single selection by id, unread and pinned marks | -| [`FlowMessage`](https://flowui.stac.dev/components/message-thread) | One turn — ink-wash user bubble, plain assistant, error bubble, typed content parts | -| [`FlowStreamingText`](https://flowui.stac.dev/components/streaming-text) | Animated text reveal while a reply arrives | -| [`FlowThinkingIndicator`](https://flowui.stac.dev/components/thinking-indicator) | Turning, breathing asterisk with a shimmering label | -| [`FlowShimmerText`](https://flowui.stac.dev/components/shimmer-text) | Sweeping text highlight, static once settled | -| [`FlowCodeBlock`](https://flowui.stac.dev/components/code-block) | Fenced code with built-in synchronous highlighting, a header label, and a copy affordance — languages host-extensible | -| [`FlowMarkdown`](https://flowui.stac.dev/components/markdown) | Assistant prose typeset from a built-in parser — headings, emphasis, lists, quotes, tables, links, and fences composing the code block; assistant turns render it by default and it streams gracefully | -| [`FlowErrorState`](https://flowui.stac.dev/components/error-state) | Failure card with a host-written message and retry pill — failed turns render it automatically | -| [`FlowConfirmation`](https://flowui.stac.dev/components/confirmation) | Approval card — an asterisk-marked request with approve and reject buttons that settles into the outcome; confirmation parts render it in a thread | -| [`FlowToast`](https://flowui.stac.dev/components/toast) | Floating notice: a glyph, one wrapping line and a cross on a frosted card; `showFlowToast` floats it in the nearest Overlay, stacked and auto-dismissing, with a handle to dismiss it early | -| [`FlowTool`](https://flowui.stac.dev/components/tool) | Tool-call card: one status mark that settles from a turning asterisk into a check or an error glyph, the call's title and primary argument, and its input and output behind a disclosure; tool parts render it in a thread | -| [`FlowMessageActions`](https://flowui.stac.dev/components/message-actions) | Copy / regenerate / edit / feedback row under a message | -| [`FlowComposer`](https://flowui.stac.dev/components/composer) | Multiline input with send/stop, attachments strip, the platform's file dialog (`showFlowAttachmentPicker` from your own menu, or a built-in attach button), image paste and card-scoped drop (web), and leading/trailing action slots | -| [`FlowMenu`](https://flowui.stac.dev/components/menu) | Icon-triggered menu with groups, submenus, and toggles — anchored card on desktop, bottom sheet on phones | -| [`FlowModelSelector`](https://flowui.stac.dev/components/model-selector) | Model picker with effort and overflow submenus, sheet on phones | -| [`FlowPill`](https://flowui.stac.dev/components/pill) | Removable pill for an enabled tool or mode in the composer's action row — label auto-drops on phones | -| [`FlowAttachmentGroup`](https://flowui.stac.dev/components/attachments) | Image and file tiles with a type pill | -| [`FlowAttachmentPreview`](https://flowui.stac.dev/components/attachments) | Full-screen image viewer with zoom and paging | -| [`FlowDropTarget`](https://flowui.stac.dev/components/chat-view) | File-drop detection for any subtree (web) — decoded attachments out, hover state for your own treatment | -| [`FlowSuggestion`](https://flowui.stac.dev/components/suggestions) / [`FlowSuggestionGroup`](https://flowui.stac.dev/components/suggestions) | Prompt starters — plain or outlined; scroll, wrap, or column layouts | -| [`FlowGreeting`](https://flowui.stac.dev/components/greeting) | Zero-state headline | -| [`FlowTheme`](https://flowui.stac.dev/theming) | Design tokens (colors and typography) as a `ThemeExtension`, with light and dark presets | - -## Getting started - -```yaml -dependencies: - flow_ui: ^0.3.0 -``` - -Install the theme once (optional — without it, components fall back to a -preset matching the ambient brightness): +## Packages -```dart -MaterialApp( - theme: ThemeData(extensions: [FlowTheme.light()]), - darkTheme: ThemeData( - brightness: Brightness.dark, - extensions: [FlowTheme.dark()], - ), -) -``` - -The default typography — Google Sans and Google Sans Code — arrives through -[google_fonts](https://pub.dev/packages/google_fonts): each cut is fetched -on first use and cached on the device, so there is no font to bundle. The -fetch needs network access: `android.permission.INTERNET` in an Android -app's main manifest (Flutter's template grants it only to debug and profile) -and the `com.apple.security.network.client` entitlement in a sandboxed macOS -app; without it text falls back to the platform face. To render offline on -first launch, ship the files under a `google_fonts/` asset folder. - -## Build a chat screen - -Messages are pure view models. Your app maps its own transport into -`FlowMessageData`, and streaming is data, not streams: while a reply arrives, -rebuild with `copyWith` carrying the grown text. - -```dart -class ChatPage extends StatefulWidget { - const ChatPage({super.key}); +This repository is a Dart pub workspace. - @override - State createState() => _ChatPageState(); -} +| Package | What it is | Status | +|---|---|---| +| [`packages/flow_ui`](packages/flow_ui) | The chat and assistant UI component library | [![pub package](https://img.shields.io/pub/v/flow_ui.svg)](https://pub.dev/packages/flow_ui) | +| [`packages/stacflow`](packages/stacflow) | Streaming AI chat on flow_ui with Gemini, OpenAI and Claude using your own key: `StacFlowChat` and `StacFlowChatView` | Pre-release, not published | -class _ChatPageState extends State { - final ScrollController _scroll = ScrollController(); - List _messages = const []; - bool _generating = false; - - void _send(String text) async { - final id = DateTime.now().microsecondsSinceEpoch.toString(); - setState(() { - _messages = [ - ..._messages, - FlowMessageData.text(id: id, role: FlowMessageRole.user, text: text), - // An empty pending reply renders the thinking indicator. - FlowMessageData( - id: '$id-reply', - role: FlowMessageRole.assistant, - status: FlowMessageStatus.pending, - ), - ]; - _generating = true; - }); - - // Feed chunks from your backend as they arrive. - var streamed = ''; - await for (final chunk in myBackend.reply(text)) { - streamed += chunk; - setState(() { - _messages = [ - ..._messages.sublist(0, _messages.length - 1), - _messages.last.copyWith( - parts: [FlowTextPart(streamed)], - status: FlowMessageStatus.streaming, - ), - ]; - }); - } - - setState(() { - _messages = [ - ..._messages.sublist(0, _messages.length - 1), - _messages.last.copyWith(status: FlowMessageStatus.complete), - ]; - _generating = false; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: FlowChatView( - empty: _messages.isEmpty, - greeting: const FlowGreeting( - icon: Icons.wb_twilight, - text: 'Good afternoon', - ), - suggestions: FlowSuggestionGroup( - layout: FlowSuggestionLayout.column, - suggestions: [ - FlowSuggestion( - label: 'Write an essay about life and enjoyment', - icon: Icons.edit_note, - onTap: () => _send('Write an essay about life and enjoyment'), - ), - FlowSuggestion( - label: 'Create a Monday briefing from my tasks', - icon: Icons.event_available, - onTap: () => _send('Create a Monday briefing from my tasks'), - ), - ], - ), - thread: FlowThread( - messages: _messages, - controller: _scroll, - thinkingLabel: 'Thinking…', - ), - threadController: _scroll, - jumpToLatestTooltip: 'Jump to latest', - composer: FlowComposer( - placeholder: 'How can I help you today?', - isStreaming: _generating, - onSend: _send, - onStop: myBackend.stop, - ), - ), - ); - } -} -``` +Also here: [`playground/`](playground), every component on a stage, hosted at [flowui.stac.dev/playground](https://flowui.stac.dev/playground); [`docs/`](docs), the site; [`contracts/`](contracts) with `contracts.lock`, the SDK wire contract and its hash, generated by the scripts in [`tool/`](tool). -`FlowChatView` is body-only — it builds no `Scaffold` and no app bar, so -your app keeps the chrome, the background, and the keyboard inset. See -[`example/lib/main.dart`](example/lib/main.dart) for a complete runnable -version of this page, and the [live playground](https://flowui.stac.dev/playground) -for a demo of every component with variants and code snippets. - -## Composer accessories - -The composer takes leading and trailing action slots. Drop in a `FlowMenu` -(attachments, toggles) and a `FlowModelSelector` — both render an anchored -card on wide layouts and a bottom sheet on phones: - -```dart -FlowComposer( - onSend: _send, - leadingActions: [ - FlowMenu( - icon: Icons.add, - tooltip: 'Add to chat', - entries: const [ - FlowMenuOption(id: 'files', icon: Icons.attach_file, label: 'Add files'), - FlowMenuDivider(), - FlowMenuOption(id: 'web', icon: Icons.public, label: 'Web search', selected: true), - ], - onSelected: _handleMenu, - ), - ], - trailingActions: [ - FlowModelSelector( - models: const [ - FlowModelOption(id: 'fast', label: 'Fast', description: 'Quick answers'), - FlowModelOption(id: 'smart', label: 'Smart', description: 'Hard problems'), - ], - selectedId: _modelId, - onSelected: (id) => setState(() => _modelId = id), - ), - ], -) -``` - -## Message content is typed parts - -A message holds an ordered list of sealed `FlowMessagePart`s — -`FlowTextPart`, `FlowAttachmentPart`, and `FlowCustomPart` for anything the -package doesn't know about. Custom parts render through a builder you supply, -so hosts can inject arbitrary widgets (tool cards, citations, charts) without -forking the message renderer: - -```dart -FlowThread( - messages: _messages, - customPartBuilder: (context, message, part) { - return switch (part.type) { - 'order-card' => OrderCard(order: part.data as Order), - _ => null, // unknown parts are skipped - }; - }, -) -``` - -Attachments carry an `ImageProvider`, so network, file, memory, and asset -images all work. Build them yourself, or let the package hand them to you -ready-made — `showFlowAttachmentPicker` opens the platform's file dialog -from your own menu (or the composer's built-in button does), and on the -web the surface takes drops and the field takes pasted images: - -```dart -FlowMessageData( - id: 'm1', - role: FlowMessageRole.user, - parts: [ - FlowAttachmentPart([ - FlowAttachment(id: 'a1', thumbnail: NetworkImage(url), kind: 'JPG', label: 'sunset.jpg'), - ]), - FlowTextPart('What do you think of this shot?'), - ], -) -``` - -## Theming - -`FlowTheme` carries two token sets — colors and typography. Role names follow -Material 3's `ColorScheme`, so an existing scheme maps across, with one -addition: the design draws content at three ink levels (`onSurface`, -`onSurfaceVariant`, `onSurfaceMuted`) where M3 names two. Start from a preset -and override what your brand needs: - -```dart -FlowTheme( - colors: FlowColors.dark.copyWith(primary: const Color(0xFF6C5CE7)), - typography: FlowTypography.standard, -) -``` - -Spacing and corner radii are deliberately not tokens. Following Material's -structure, each component bakes its own metrics from the Flow UI design file -and exposes per-widget overrides (`padding:`, `borderRadius:`) where hosts -retheme. Strings shown to the user (tooltips, placeholders, labels) are -host-supplied, so localization stays in your app — the one exception, the -model selector's `effortLabel` and `moreModelsLabel` English defaults, is -overridable the same way. - -## Docs & playground - -Full documentation lives at [flowui.stac.dev](https://flowui.stac.dev/), and -every component has a stage in the -[live playground](https://flowui.stac.dev/playground) — variant pills and -code snippets included. The playground is also -[in the repo](https://github.com/StacDev/flow_ui/tree/main/playground) to run -locally: +## Development ```bash +flutter pub get # resolves the whole workspace +dart run melos run analyze +dart run melos run test cd playground && flutter run -d chrome ``` -## License - -Code is released under the [MIT License](LICENSE). Google Sans and Google -Sans Code are fetched from Google Fonts under the -[SIL Open Font License](https://openfontlicense.org), not bundled. +Package docs: [flow_ui README](packages/flow_ui/README.md). Licensed under [MIT](LICENSE). diff --git a/analysis_options.yaml b/analysis_options.yaml index 534f26d..376dd57 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,13 +1,15 @@ +# Governs tool/ and the stacflow package. packages/flow_ui, its example and +# playground keep their own flutter_lints files (nearest file wins). +include: package:very_good_analysis/analysis_options.yaml + analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true exclude: - - build/** - - android/** - - ios/** - - web/** - - windows/** - - macos/** - - linux/** -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + - "**/build/**" + - "**/lib/src/generated/**" + - docs/** + errors: + public_member_api_docs: ignore diff --git a/contracts.lock b/contracts.lock new file mode 100644 index 0000000..66dedc1 --- /dev/null +++ b/contracts.lock @@ -0,0 +1 @@ +63057a526571aa2cfd11a856843497730cfdc54b688d4fde8dc5f1372be22c3f diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..459ed7d --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,19 @@ +# contracts/ — the single source of truth + +The StacFlow wire contract is authored **here** and only here. TypeScript +(gateway) and Dart (SDK + console client) types are generated from these +files — never hand-written. A bot PR mirrors this tree into +`StacDev/stacflow-cloud`; CI on both repos fails when the canonical hashes +differ (`tool/contracts_hash.sh`). + +| File | What it defines | +|---|---| +| `sse-events.schema.json` | The SSE streaming protocol v1: seven events, framing rules, ordering invariants | +| `rest-api.openapi.yaml` | The `/v1` REST surface (OpenAPI 3.1) | +| `error-codes.json` | The frozen machine-readable error-code registry + failure-class attribution | +| `a2ui/` | Vendored A2UI v0.9.x schemas (generative UI payloads; consumed from M2) | + +**Status: v1-draft.** Field names freeze at the M1 exit — after all three +provider conformance suites and the console playground consume the protocol. +After the freeze, evolution is **additive only**: new event names, new optional +fields, new error codes. Decoders never throw on unknown events or fields. diff --git a/contracts/a2ui/README.md b/contracts/a2ui/README.md new file mode 100644 index 0000000..3cfd1b7 --- /dev/null +++ b/contracts/a2ui/README.md @@ -0,0 +1,14 @@ +# Vendored A2UI schemas — pending pin + +This directory will hold the **vendored A2UI v0.9.x JSON schemas** (component +messages, data-model messages, `renderer_to_agent` egress) copied verbatim from +the upstream A2UI project at a pinned release/commit recorded here. + +Not vendored yet: the pin (exact upstream tag + commit) is chosen at the start +of the M2 GenUI work, when the codec spike consumes these files — vendoring a +moving spec earlier would freeze an arbitrary snapshot nobody validates +against. Until then, nothing imports this directory. + +When vendoring: copy files unmodified, record `UPSTREAM.md` with the source +URL, tag, commit hash, and licence, and never edit the vendored files — the +codec owns all adaptation. diff --git a/contracts/error-codes.json b/contracts/error-codes.json new file mode 100644 index 0000000..30d59c3 --- /dev/null +++ b/contracts/error-codes.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "description": "The frozen, additively growing error-code registry. `failure_class` is the D18 attribution enum; `http` is the status the REST surface uses; SSE error events carry the same code/class. Clients map `code` to localized strings — `message` on the wire is user-safe English.", + "codes": { + "invalid_api_key": { "failure_class": "gateway", "http": 401, "retryable": false, "description": "Publishable key missing, malformed, revoked, or hash mismatch." }, + "unauthorized": { "failure_class": "gateway", "http": 401, "retryable": false, "description": "Missing or invalid credentials for this endpoint." }, + "forbidden": { "failure_class": "gateway", "http": 403, "retryable": false, "description": "The authenticated member lacks permission for this action." }, + "token_expired": { "failure_class": "gateway", "http": 401, "retryable": true, "description": "Session JWT expired; the SDK recreates the session transparently and retries." }, + "unknown_agent": { "failure_class": "gateway", "http": 404, "retryable": false, "description": "agent_id does not exist in this project." }, + "agent_not_deployed": { "failure_class": "gateway", "http": 404, "retryable": false, "description": "No deployment exists for (agent, environment)." }, + "unknown_manifest": { "failure_class": "gateway", "http": 409, "retryable": true, "description": "manifest_hash not registered; POST the body to /v1/manifests and retry." }, + "duplicate_message": { "failure_class": "gateway", "http": 409, "retryable": false, "description": "client_msg_id already processed; details.turn_id carries the existing turn." }, + "thread_not_found": { "failure_class": "gateway", "http": 404, "retryable": false, "description": "Thread does not exist or is not visible to this end user." }, + "validation_failed": { "failure_class": "gateway", "http": 422, "retryable": false, "description": "Request body failed contract validation; details carries locators." }, + "quota_exceeded": { "failure_class": "policy", "http": 429, "retryable": false, "description": "A project quota or plan cap tripped; a clear refusal, never a mystery 500." }, + "missing_approval": { "failure_class": "policy", "http": 422, "retryable": false, "description": "A destructive tool_results entry arrived without an approval object." }, + "jti_reused": { "failure_class": "policy", "http": 409, "retryable": false, "description": "Single-use authorization id was already consumed; the continuation is rejected." }, + "pending_tools_unanswered": { "failure_class": "policy", "http": 422, "retryable": false, "description": "The continuation left one or more pending tool calls without a result." }, + "provider_error": { "failure_class": "provider", "http": 502, "retryable": true, "description": "The model provider failed; upstream carries {provider, status, code}." }, + "provider_rate_limited": { "failure_class": "provider", "http": 429, "retryable": true, "description": "The developer's provider key is rate-limited or out of quota upstream." }, + "provider_auth_failed": { "failure_class": "provider", "http": 502, "retryable": false, "description": "The vaulted BYOK key was rejected by the provider; fix it in Console → Settings → provider keys." }, + "segment_timeout": { "failure_class": "timeout", "http": 504, "retryable": true, "description": "The provider stream stalled past the segment deadline." }, + "cancelled": { "failure_class": "cancelled", "http": 499, "retryable": false, "description": "The client stopped the stream; the segment ends done{cancelled}." }, + "internal": { "failure_class": "gateway", "http": 500, "retryable": true, "description": "Unattributed gateway fault; alarmed and triaged — never the catch-all for the classes above." } + } +} diff --git a/contracts/rest-api.openapi.yaml b/contracts/rest-api.openapi.yaml new file mode 100644 index 0000000..9a8bcde --- /dev/null +++ b/contracts/rest-api.openapi.yaml @@ -0,0 +1,676 @@ +openapi: 3.1.0 +info: + title: StacFlow API + version: 1.0.0-draft + license: + name: MIT + identifier: MIT + description: >- + The StacFlow /v1 surface consumed by the SDK and the console's generated + client. v1-draft: field names freeze at the M1 exit; evolution is additive + only thereafter. The SSE stream returned by the messages endpoint is + specified in sse-events.schema.json. Error codes and their failure-class + attribution live in error-codes.json. The console API (/console/v1/*) is + added when its routes land (step B1); /v1/ingest lands in M3. +servers: + - url: https://api.stacflow.ai +security: [] + +paths: + /v1/sessions: + post: + operationId: createSession + summary: Start a session — resolve the deployed agent version, mint a session token. + security: + - publishableKey: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSessionRequest' + responses: + '201': + description: Session created. + content: + application/json: + schema: + $ref: '#/components/schemas/Session' + '401': + $ref: '#/components/responses/Error' + '404': + $ref: '#/components/responses/Error' + '409': + description: >- + unknown_manifest — POST the full manifest to /v1/manifests + (authenticating with the publishable key), then retry. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + '429': + description: quota_exceeded — a project quota or plan cap tripped. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + + /v1/sessions/{session_id}/messages: + post: + operationId: postMessage + summary: >- + Send a user message or continue a turn with tool results. The response + is one model segment as an SSE stream (see sse-events.schema.json). + security: + - sessionToken: [] + parameters: + - $ref: '#/components/parameters/SessionId' + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/UserMessageBody' + - $ref: '#/components/schemas/ToolResultsBody' + discriminator: + propertyName: kind + mapping: + user_message: '#/components/schemas/UserMessageBody' + tool_results: '#/components/schemas/ToolResultsBody' + responses: + '200': + description: >- + The normalized event stream. Events, framing, and ordering + invariants are defined by sse-events.schema.json; every stream ends + with exactly one `done`. + content: + text/event-stream: + schema: + type: string + description: SSE frames per contracts/sse-events.schema.json. + '401': + description: Includes code token_expired — recreate the session transparently. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + '404': + description: thread_not_found — the thread_id does not exist or is not visible to this end user. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + '409': + description: >- + duplicate_message (idempotent replay — the envelope carries the + existing turn_id in details) or unknown_manifest. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + '422': + $ref: '#/components/responses/Error' + '429': + description: quota_exceeded — a project quota or plan cap tripped. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + + /v1/manifests: + post: + operationId: registerManifest + summary: >- + Register a capability manifest body for its hash. Manifests are + immutable and cached by hash forever; registration is idempotent. + Accepts the publishable key as well as a session token, because the + 409 unknown_manifest recovery on session creation happens before any + session token exists. + security: + - publishableKey: [] + - sessionToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CapabilityManifest' + responses: + '201': + description: Registered (or already known). + content: + application/json: + schema: + type: object + properties: + manifest_hash: + $ref: '#/components/schemas/ManifestHash' + required: [manifest_hash] + '422': + $ref: '#/components/responses/Error' + + /v1/threads: + get: + operationId: listThreads + summary: List the session end-user's threads, newest first. + security: + - sessionToken: [] + parameters: + - $ref: '#/components/parameters/Cursor' + - $ref: '#/components/parameters/Limit' + responses: + '200': + description: One page of threads. + content: + application/json: + schema: + type: object + properties: + threads: + type: array + items: + $ref: '#/components/schemas/Thread' + next_cursor: + type: [string, 'null'] + required: [threads] + '401': + $ref: '#/components/responses/Error' + + /v1/threads/{thread_id}: + get: + operationId: getThread + summary: Fetch a thread with a page of its messages (server is the source of truth). + security: + - sessionToken: [] + parameters: + - $ref: '#/components/parameters/ThreadId' + - $ref: '#/components/parameters/Cursor' + - $ref: '#/components/parameters/Limit' + responses: + '200': + description: The thread and one page of messages, oldest first within the page. + content: + application/json: + schema: + type: object + properties: + thread: + $ref: '#/components/schemas/Thread' + messages: + type: array + items: + $ref: '#/components/schemas/Message' + next_cursor: + type: [string, 'null'] + required: [thread, messages] + '404': + $ref: '#/components/responses/Error' + patch: + operationId: updateThread + summary: Rename a thread. + security: + - sessionToken: [] + parameters: + - $ref: '#/components/parameters/ThreadId' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + maxLength: 200 + required: [title] + responses: + '200': + description: Updated. + content: + application/json: + schema: + $ref: '#/components/schemas/Thread' + '404': + $ref: '#/components/responses/Error' + delete: + operationId: deleteThread + summary: Delete a thread and its messages. + security: + - sessionToken: [] + parameters: + - $ref: '#/components/parameters/ThreadId' + responses: + '204': + description: Deleted. + '404': + $ref: '#/components/responses/Error' + +components: + securitySchemes: + publishableKey: + type: http + scheme: bearer + description: 'Authorization: Bearer sf_{env}_{projectId}_{keyId}_{secret}' + sessionToken: + type: http + scheme: bearer + bearerFormat: JWT + description: EdDSA session JWT from POST /v1/sessions; TTL 60 minutes. + + parameters: + SessionId: + name: session_id + in: path + required: true + schema: + type: string + pattern: '^ses_[A-Za-z0-9]+$' + ThreadId: + name: thread_id + in: path + required: true + schema: + $ref: '#/components/schemas/ThreadId' + Cursor: + name: cursor + in: query + required: false + schema: + type: string + Limit: + name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 50 + + responses: + Error: + description: Error envelope; code + failure_class per error-codes.json. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + + schemas: + ThreadId: + type: string + pattern: '^thr_[A-Za-z0-9]+$' + TurnId: + type: string + pattern: '^trn_[A-Za-z0-9]+$' + MessageId: + type: string + pattern: '^msg_[A-Za-z0-9]+$' + ToolCallId: + type: string + pattern: '^tc_[A-Za-z0-9]+$' + ManifestHash: + type: string + pattern: '^cap_[A-Za-z0-9_-]+$' + description: base64url of the first 128 bits of SHA-256 over canonical manifest JSON. + FailureClass: + type: string + enum: [provider, developer_api, gateway, policy, timeout, cancelled] + + CreateSessionRequest: + type: object + properties: + agent_id: + type: string + pattern: '^agt_[A-Za-z0-9]+$' + environment: + type: string + enum: [dev, prod] + external_user_id: + type: string + minLength: 1 + maxLength: 256 + description: >- + End-user identity, HMAC-hashed server-side and never stored raw. + Developer-supplied when the app has a login; in anonymous mode the + SDK supplies its persisted device-scoped id here and sets + `anonymous` (documented as counting installs). + anonymous: + type: boolean + default: false + description: True when external_user_id is an SDK device id (D19). + manifest_hash: + $ref: '#/components/schemas/ManifestHash' + required: [agent_id, environment, external_user_id] + + Session: + type: object + properties: + session_id: + type: string + pattern: '^ses_[A-Za-z0-9]+$' + session_token: + type: string + expires_at: + type: string + format: date-time + agent_version: + $ref: '#/components/schemas/AgentVersionInfo' + required: [session_id, session_token, expires_at, agent_version] + + AgentVersionInfo: + type: object + description: The resolved deployment for (agent, environment) — metadata only, never the prompt. + properties: + agent_version_id: + type: string + pattern: '^agv_[A-Za-z0-9]+$' + model: + type: string + display_name: + type: string + required: [agent_version_id, model] + + UserMessageBody: + type: object + properties: + kind: + const: user_message + thread_id: + $ref: '#/components/schemas/ThreadId' + description: Absent — the gateway creates a thread. + client_msg_id: + type: string + format: uuid + description: Outbox idempotency key; a duplicate returns 409 with the existing turn_id. + parts: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/UserPart' + manifest_hash: + $ref: '#/components/schemas/ManifestHash' + required: [kind, client_msg_id, parts, manifest_hash] + + UserPart: + type: object + description: v1 carries text parts only; attachments are post-v1 (additive). + properties: + type: + const: text + text: + type: string + required: [type, text] + + ToolResultsBody: + type: object + description: >- + Continues a turn whose last segment ended awaiting_client_tools. The + gateway rejects it if any pending call is unanswered, if a destructive + result lacks approval (policy), or on jti reuse. + properties: + kind: + const: tool_results + thread_id: + $ref: '#/components/schemas/ThreadId' + turn_id: + $ref: '#/components/schemas/TurnId' + results: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ToolResultEntry' + required: [kind, thread_id, turn_id, results] + + ToolResultEntry: + type: object + properties: + tool_call_id: + $ref: '#/components/schemas/ToolCallId' + status: + type: string + enum: [ok, error, declined] + result: + type: object + additionalProperties: true + description: Bounded (64 KB); the forModel-reduced value. + error: + $ref: '#/components/schemas/ToolError' + approval: + $ref: '#/components/schemas/Approval' + required: [tool_call_id, status] + + ToolError: + type: object + properties: + failure_class: + $ref: '#/components/schemas/FailureClass' + code: + type: string + message: + type: string + upstream: + type: object + properties: + provider: + type: string + status: + type: integer + code: + type: string + required: [failure_class, code, message] + + Approval: + type: object + description: Required on every destructive result. Provenance is set by the confirmation surface, never the caller. + properties: + provenance: + type: string + enum: [human_confirmation, biometric] + decision_fingerprint: + type: string + description: 'sha256 over canonicalJson(args) ‖ renderedCard, prefixed "sha256:".' + jti: + type: string + description: Single-use authorization id; consumed server-side as defense-in-depth. + required: [provenance, decision_fingerprint, jti] + + CapabilityManifest: + type: object + description: >- + Assembled from the live registries, never hand-written. Canonical JSON + (sorted keys, UTF-8, no whitespace) hashes to the cap_ ManifestHash. + build/platform/locale ride as headers, never inside the hashed body. + properties: + manifestVersion: + type: integer + const: 1 + sdk: + type: object + properties: + package: + type: string + version: + type: string + required: [package, version] + catalogs: + type: array + items: + type: string + components: + type: object + additionalProperties: + type: integer + tools: + type: object + additionalProperties: + type: object + properties: + v: + type: integer + capabilityId: + type: string + permission: + type: string + enum: [read, write, destructive] + confirmation: + type: boolean + biometric: + type: boolean + description: + type: string + description: Model-facing tool description (dev-plan §6.4). + args_schema: + type: object + additionalProperties: true + description: >- + JSON Schema 2020-12 for the tool's arguments — authored once + here; the gateway (or DirectTransport) compiles it per + provider (§6.4). The client gate still enforces the full + schema, so provider-side laxity never becomes an execution + hole. + required: [v, permission] + policies: + type: object + additionalProperties: + type: integer + capabilities: + type: object + additionalProperties: + type: integer + limits: + type: object + additionalProperties: + type: integer + required: [manifestVersion, sdk, tools] + + Thread: + type: object + properties: + thread_id: + $ref: '#/components/schemas/ThreadId' + title: + type: [string, 'null'] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + last_message_at: + type: [string, 'null'] + format: date-time + required: [thread_id, created_at, updated_at] + + Message: + type: object + description: One user or assistant message; assistant messages accumulate ordered parts across a turn's segments. + properties: + message_id: + $ref: '#/components/schemas/MessageId' + thread_id: + $ref: '#/components/schemas/ThreadId' + turn_id: + $ref: '#/components/schemas/TurnId' + role: + type: string + enum: [user, assistant] + created_at: + type: string + format: date-time + parts: + type: array + items: + $ref: '#/components/schemas/MessagePart' + required: [message_id, thread_id, turn_id, role, created_at, parts] + + MessagePart: + oneOf: + - $ref: '#/components/schemas/TextPart' + - $ref: '#/components/schemas/ToolCallPart' + - $ref: '#/components/schemas/UiSurfacePart' + discriminator: + propertyName: type + mapping: + text: '#/components/schemas/TextPart' + tool_call: '#/components/schemas/ToolCallPart' + ui_surface: '#/components/schemas/UiSurfacePart' + + TextPart: + type: object + properties: + type: + const: text + text: + type: string + required: [type, text] + + ToolCallPart: + type: object + description: A tool call and (once continued) its result, as persisted for replay. + properties: + type: + const: tool_call + tool_call_id: + $ref: '#/components/schemas/ToolCallId' + tool_name: + type: string + executor: + type: string + enum: [client, server] + permission_class: + type: string + enum: [read, write, destructive] + args: + type: object + additionalProperties: true + status: + type: string + enum: [pending, ok, error, declined] + result: + type: object + additionalProperties: true + error: + $ref: '#/components/schemas/ToolError' + approval: + $ref: '#/components/schemas/Approval' + required: [type, tool_call_id, tool_name, executor, permission_class, args, status] + + UiSurfacePart: + type: object + description: >- + A rendered-UI surface: the ordered A2UI messages that built it, for + replay. + properties: + type: + const: ui_surface + surface_id: + type: string + a2ui: + type: array + items: + type: object + additionalProperties: true + required: [type, surface_id, a2ui] + + ErrorEnvelope: + type: object + properties: + error: + type: object + properties: + code: + type: string + description: From error-codes.json. + failure_class: + $ref: '#/components/schemas/FailureClass' + message: + type: string + retryable: + type: boolean + details: + type: object + additionalProperties: true + description: 'Code-specific payload, e.g. {"turn_id": ...} on duplicate_message.' + required: [code, failure_class, message, retryable] + required: [error] diff --git a/contracts/sse-events.schema.json b/contracts/sse-events.schema.json new file mode 100644 index 0000000..c11398a --- /dev/null +++ b/contracts/sse-events.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stacflow.ai/contracts/sse-events.schema.json", + "title": "StacFlow SSE protocol v1", + "description": "The normalized event stream returned by POST /v1/sessions/{session_id}/messages. Framing: Content-Type text/event-stream; the SSE `event:` field carries the event name; `data:` carries exactly one JSON object on one line; the SSE `id:` field carries the decimal `seq`. A comment heartbeat `: ping` is sent every 15 seconds. `seq` is 0-based and strictly monotonic per stream with no gaps. Stream-scoped identity is stated once in `start` and never repeated. Every stream ends with exactly one `done`; a socket close without `done` is a retryable transport failure, never a protocol state. This schema is also the internal adapter contract: local mode's Dart provider adapters emit the same event union. Evolution is additive-only after the v1 freeze; decoders must ignore unknown event names and unknown fields.", + "x-stacflow-protocol": { + "version": 1, + "events": { + "start": "#/$defs/StartEvent", + "delta": "#/$defs/DeltaEvent", + "tool_call": "#/$defs/ToolCallEvent", + "ui_payload": "#/$defs/UiPayloadEvent", + "usage": "#/$defs/UsageEvent", + "error": "#/$defs/ErrorEvent", + "done": "#/$defs/DoneEvent" + }, + "reserved": { + "tool_result": "Reserved for a future gateway-executed tool lane; in v1 all tools execute on the client and results travel in the tool_results continuation POST.", + "resume": "The seq / SSE id discipline exists so a Last-Event-ID resume endpoint can be added additively; v1 reconnect = refetch the thread and re-POST idempotently." + }, + "ordering": [ + "Per tool call: phase start, then zero or more phase delta, then phase end.", + "Assistant text may interleave BETWEEN tool calls, never inside one call's phases.", + "Tool calls are serialized in v1: no overlapping starts.", + "`error` is non-terminal by itself and is always followed by `done`.", + "`done` is always last and occurs exactly once." + ] + }, + "oneOf": [ + { "$ref": "#/$defs/StartEvent" }, + { "$ref": "#/$defs/DeltaEvent" }, + { "$ref": "#/$defs/ToolCallEvent" }, + { "$ref": "#/$defs/UiPayloadEvent" }, + { "$ref": "#/$defs/UsageEvent" }, + { "$ref": "#/$defs/ErrorEvent" }, + { "$ref": "#/$defs/DoneEvent" } + ], + "$defs": { + "Seq": { + "type": "integer", + "minimum": 0, + "description": "0-based, strictly monotonic per stream, no gaps. Mirrored in the SSE id: field." + }, + "TurnId": { "type": "string", "pattern": "^trn_[A-Za-z0-9]+$" }, + "ThreadId": { "type": "string", "pattern": "^thr_[A-Za-z0-9]+$" }, + "MessageId": { "type": "string", "pattern": "^msg_[A-Za-z0-9]+$" }, + "ToolCallId": { "type": "string", "pattern": "^tc_[A-Za-z0-9]+$" }, + "AgentVersionId": { "type": "string", "pattern": "^agv_[A-Za-z0-9]+$" }, + "FailureClass": { + "type": "string", + "enum": ["provider", "developer_api", "gateway", "policy", "timeout", "cancelled"], + "description": "The wire-level attribution enum (D18), shared by SSE error events, tool_results entries, and analytics." + }, + "Upstream": { + "type": "object", + "description": "Structured upstream attribution: which provider/backend actually failed, regardless of the observed HTTP status.", + "properties": { + "provider": { "type": "string" }, + "status": { "type": "integer" }, + "code": { "type": "string" } + }, + "additionalProperties": true + }, + "StartEvent": { + "type": "object", + "description": "First event of every stream (seq 0). Declares stream-scoped identity once. `segment` is the 0-based continuation index within the turn; `message_id` is the assistant message being built, stable across all segments of the turn.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "protocol": { "const": 1 }, + "turn_id": { "$ref": "#/$defs/TurnId" }, + "segment": { "type": "integer", "minimum": 0 }, + "message_id": { "$ref": "#/$defs/MessageId" }, + "thread_id": { "$ref": "#/$defs/ThreadId" }, + "model": { "type": "string" }, + "agent_version_id": { "$ref": "#/$defs/AgentVersionId" } + }, + "required": ["seq", "protocol", "turn_id", "segment", "message_id", "thread_id", "model", "agent_version_id"], + "additionalProperties": true + }, + "DeltaEvent": { + "type": "object", + "description": "Assistant text delta.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "text": { "type": "string" } + }, + "required": ["seq", "text"], + "additionalProperties": true + }, + "ToolCallEvent": { + "type": "object", + "description": "One tool call, streamed in three phases. `args` at phase end is complete canonical JSON and is authoritative over concatenated deltas. `permission_class` comes from the merged manifest so the SDK can pre-stage confirmation chrome before args finish streaming. `executor` is \"client\" in v1 (\"server\" reserved).", + "oneOf": [ + { + "type": "object", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "phase": { "const": "start" }, + "tool_call_id": { "$ref": "#/$defs/ToolCallId" }, + "tool_name": { "type": "string" }, + "executor": { "type": "string", "enum": ["client", "server"] }, + "permission_class": { "type": "string", "enum": ["read", "write", "destructive"] } + }, + "required": ["seq", "phase", "tool_call_id", "tool_name", "executor", "permission_class"], + "additionalProperties": true + }, + { + "type": "object", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "phase": { "const": "delta" }, + "tool_call_id": { "$ref": "#/$defs/ToolCallId" }, + "args_delta": { "type": "string", "description": "Raw JSON fragment; enables progressive argument rendering. Consumers may ignore deltas entirely." } + }, + "required": ["seq", "phase", "tool_call_id", "args_delta"], + "additionalProperties": true + }, + { + "type": "object", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "phase": { "const": "end" }, + "tool_call_id": { "$ref": "#/$defs/ToolCallId" }, + "args": { "type": "object", "additionalProperties": true } + }, + "required": ["seq", "phase", "tool_call_id", "args"], + "additionalProperties": true + } + ] + }, + "UiPayloadEvent": { + "type": "object", + "description": "Exactly one complete A2UI v0.9 JSONL message, extracted and re-framed by the gateway (buffered to newline / tool-end server-side). Opaque to the gateway beyond size caps; validated client-side against the registered catalog.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "surface_id": { "type": "string" }, + "a2ui": { "type": "object", "additionalProperties": true } + }, + "required": ["seq", "surface_id", "a2ui"], + "additionalProperties": true + }, + "UsageEvent": { + "type": "object", + "description": "One per provider call; additive across a stream. Informational to the client — the gateway's persisted record is authoritative for metering.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "provider_call": { "type": "integer", "minimum": 0 }, + "model": { "type": "string" }, + "input_tokens": { "type": "integer", "minimum": 0 }, + "output_tokens": { "type": "integer", "minimum": 0 } + }, + "required": ["seq", "provider_call", "model", "input_tokens", "output_tokens"], + "additionalProperties": true + }, + "ErrorEvent": { + "type": "object", + "description": "Non-terminal by itself; always followed by done. `code` comes from error-codes.json (additively growing); `message` is user-safe English — clients map `code` to localized strings.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "failure_class": { "$ref": "#/$defs/FailureClass" }, + "code": { "type": "string" }, + "message": { "type": "string" }, + "retryable": { "type": "boolean" }, + "upstream": { "$ref": "#/$defs/Upstream" } + }, + "required": ["seq", "failure_class", "code", "message", "retryable"], + "additionalProperties": true + }, + "DoneEvent": { + "type": "object", + "description": "Always last, exactly once. `awaiting_client_tools` ends the segment pending client execution; the turn continues via a tool_results POST.", + "properties": { + "seq": { "$ref": "#/$defs/Seq" }, + "status": { "type": "string", "enum": ["complete", "awaiting_client_tools", "error", "cancelled"] }, + "turn_id": { "$ref": "#/$defs/TurnId" }, + "pending_tool_call_ids": { + "type": "array", + "items": { "$ref": "#/$defs/ToolCallId" }, + "minItems": 1 + } + }, + "required": ["seq", "status", "turn_id"], + "if": { "properties": { "status": { "const": "awaiting_client_tools" } } }, + "then": { "required": ["seq", "status", "turn_id", "pending_tool_call_ids"] }, + "additionalProperties": true + } + } +} diff --git a/docs/README.md b/docs/README.md index ed869b1..a0d29e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ npm run build # static site in dist/ Google Sans loads from Google Fonts via the `` in `astro.config.mjs` — the same source the package fetches from at runtime. Brand colors in `src/styles/theme.css` mirror the package's design tokens in -`lib/src/theme/flow_colors.dart`. +`packages/flow_ui/lib/src/theme/flow_colors.dart`. `npm run playground` builds the repo's `playground/` Flutter app for the web into `public/playground/` — served at `/playground/` (path diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index c62fce2..6474f9b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -139,6 +139,10 @@ export default defineConfig({ label: 'Start here', items: ['getting-started', 'theming'], }, + { + label: 'StacFlow', + items: ['stacflow/getting-started', 'stacflow/providers', 'stacflow/tools'], + }, { label: 'Components', items: [{ autogenerate: { directory: 'components' } }], diff --git a/docs/src/content/docs/getting-started.mdx b/docs/src/content/docs/getting-started.mdx index f32c335..0aa3e26 100644 --- a/docs/src/content/docs/getting-started.mdx +++ b/docs/src/content/docs/getting-started.mdx @@ -14,13 +14,19 @@ Its components draw the conversation state you hand them and report what the user did through callbacks; everything model-shaped stays in your app. + + ## Install One line in your pubspec: ```yaml title="pubspec.yaml" dependencies: - flow_ui: ^0.3.0 + flow_ui: ^0.4.0 ``` That brings in the Flutter SDK, `material_ui`, `file_selector` — the @@ -94,9 +100,10 @@ snippets included. It is also in the repo to run locally: cd playground && flutter run -d chrome ``` -For a minimal starting point, the repo's `example/` is the full chat screen -from the README as a single-file app: generate platform runners once with -`flutter create . --platforms=web`, then `flutter run -d chrome`. +For a minimal starting point, the repo's `packages/flow_ui/example/` is the +full chat screen from the README as a single-file app, platform runners +checked in: `cd packages/flow_ui/example`, then +`flutter run -d chrome --dart-define=GEMINI_API_KEY=...`. ## Next diff --git a/docs/src/content/docs/stacflow/getting-started.mdx b/docs/src/content/docs/stacflow/getting-started.mdx new file mode 100644 index 0000000..b9d9594 --- /dev/null +++ b/docs/src/content/docs/stacflow/getting-started.mdx @@ -0,0 +1,156 @@ +--- +title: Getting started +description: Add stacflow, pass your own API key, and ship a streaming chat screen on flow_ui. +sidebar: + order: 1 +--- + +import { Aside } from '@astrojs/starlight/components'; + +stacflow is the runtime under flow_ui's components: a chat controller, +a ready-made screen, and adapters that talk to Gemini, OpenAI and Claude +directly with your own API key. Nothing else runs between your app and +the model. + +## Install + +```yaml title="pubspec.yaml" +dependencies: + stacflow: ^0.1.0 +``` + +One import brings flow_ui along, so every widget it exports is available +too. + +## A chat screen + +```dart title="main.dart" +import 'package:material_ui/material_ui.dart'; +import 'package:stacflow/stacflow.dart'; + +void main() => runApp(const App()); + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData(extensions: [FlowTheme.light()]), + darkTheme: ThemeData( + brightness: Brightness.dark, + extensions: [FlowTheme.dark()], + ), + home: const ChatScreen(), + ); +} + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + late final chat = StacFlowChat( + provider: GeminiProvider( + apiKey: const String.fromEnvironment('GEMINI_API_KEY'), + ), + ); + + @override + void dispose() { + chat.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => + Scaffold(body: StacFlowChatView(chat: chat)); +} +``` + +```bash +flutter run --dart-define=GEMINI_API_KEY=AIza... +``` + +The view streams the reply behind a thinking indicator, renders markdown +and code, stops mid-reply, retries a failed turn from its error card, +regenerates the latest reply, takes images from the picker, a drop or a +paste, copies replies, records feedback, shows a model selector when +the provider lists more than one model, and runs the tools you register. + +The same screen is checked in as +[the example app](https://github.com/StacDev/flow_ui/tree/main/packages/stacflow/example), +runnable against Gemini. + +## Keys + + + +Keep an `env.json` out of git: + +```json title="env.json" +{ "GEMINI_API_KEY": "AIza..." } +``` + +```bash +flutter run --dart-define-from-file=env.json +``` + +With no key the app still runs; sending answers with the error card. + +## Instructions and models + +```dart +StacFlowChat( + provider: AnthropicProvider( + apiKey: key, + models: const [ + ModelOption(id: 'claude-opus-5', label: 'Claude Opus 5'), + ModelOption(id: 'claude-sonnet-5', label: 'Claude Sonnet 5'), + ], + ), + agent: const AgentConfig( + instructions: 'You are a concise assistant.', + maxOutputTokens: 4000, + ), +) +``` + +`AgentConfig` carries the system instructions, `temperature` and +`maxOutputTokens`; unset fields are left out of the request. `models` +feeds the selector in the composer; `chat.selectModel` switches for the +next turn. + +## Headless + +`StacFlowChat` works without the view. `chat.state` is a +`ValueListenable` holding the messages, whether a reply is +streaming, the pending attachments, the last error, and the last turn's +first-token time and token counts. `send`, `stop`, `retry`, `regenerate`, +`editAndResend`, `addAttachments`, `setFeedback`, `selectModel` and +`newConversation` drive it, and any flow_ui layout renders +`state.messages`. + +## Strings + +Every string the SDK shows comes from `FlowStrings`, with English +defaults. Subclass it, override what you need, and pass it to the chat or +to the view. + +## Tools + +Register the actions the model may take with `StacFlowChat(tools:)`; +the thread shows a card per call and asks before destructive ones. See +[Tools](/stacflow/tools/). + +## Not in 0.1 + +Thread persistence, generative UI and image generation. The wire types +are public, so a custom provider can be written today by implementing +`StacFlowProvider`. diff --git a/docs/src/content/docs/stacflow/providers.mdx b/docs/src/content/docs/stacflow/providers.mdx new file mode 100644 index 0000000..b0da175 --- /dev/null +++ b/docs/src/content/docs/stacflow/providers.mdx @@ -0,0 +1,100 @@ +--- +title: Providers +description: Gemini, OpenAI and Claude, called directly with your own key, and how each one is configured. +sidebar: + order: 2 +--- + +Every provider streams the reply, takes image attachments as input, +runs the tools you register, and maps the model's failures to the same +error card. Each constructor takes +`model`, `baseUrl`, extra `headers`, an `httpClient`, the `models` a +selector should offer, and `firstByteTimeout` and `idleTimeout`. + +## Gemini + +```dart +GeminiProvider( + apiKey: const String.fromEnvironment('GEMINI_API_KEY'), + model: 'gemini-3.6-flash', +) +``` + +An AI Studio key. Works from the browser as well as from apps. Image +input: PNG, JPEG, WebP, HEIC and HEIF. Tool schemas are sent in the +OpenAPI subset Gemini accepts, so keywords outside it are dropped; Gemini +3 models keep their default temperature best, and their call ids and +thought signatures are echoed back within the turn as they require. + +## OpenAI, and compatible servers + +```dart +OpenAIProvider( + apiKey: const String.fromEnvironment('OPENAI_API_KEY'), + model: 'gpt-5-mini', +) +``` + +Speaks Chat Completions, which is what every compatible server also +implements. Point `baseUrl` at Ollama, Groq, OpenRouter, LM Studio, vLLM +or a proxy of your own: + +```dart +OpenAIProvider( + apiKey: 'ollama', + model: 'llama3.2', + baseUrl: 'http://localhost:11434/v1', + includeUsage: false, +) +``` + +`includeUsage` asks for token counts in the final chunk; turn it off for +servers that reject `stream_options`. Reasoning models may reject a +`temperature`. In the browser, Ollama needs `OLLAMA_ORIGINS` and LM Studio +needs CORS enabled. Image input: PNG, JPEG, GIF and WebP. Tools work on +the GPT-5 family and on compatible servers that implement Chat +Completions tool calls; GPT-6 Astra requires the Responses API for tools, +which this provider does not speak. + +## Claude + +```dart +AnthropicProvider( + apiKey: const String.fromEnvironment('ANTHROPIC_API_KEY'), + model: 'claude-opus-5', +) +``` + +`claude-sonnet-5` is the cheaper model. Requests carry the header +Anthropic requires for direct browser calls, so a web build works, and +the key travels to the browser with it: keep that to demos. The API needs +a reply length limit; `maxTokens` defaults to 64000 unless the agent sets +`maxOutputTokens`. Setting `temperature` on Opus 5 or Sonnet 5 is +rejected by the API and shows up as an error card. Image input: JPEG, +PNG, GIF and WebP. Opus 5 and Sonnet 5 think by default; within a turn +the provider replays the assistant's content verbatim on the tool-result +continuation, thinking blocks included, as the API requires. + +## Errors + +A failed turn keeps whatever text arrived and closes with an error card. +`ChatState.error` carries a `ChatError` with a stable `code` +(`provider_auth_failed`, `provider_rate_limited`, `validation_failed`, +`provider_error`, `segment_timeout`, `network`, `tool_rounds_exceeded`), who is responsible, the +provider's own message when there is one, and whether a retry makes +sense. The card's wording comes from `FlowStrings`. + +## Writing a provider + +Implement `StacFlowProvider`: `run(TurnRequest)` gets the conversation +as `WireMessage`s plus the `AgentConfig`, the `tools` to declare and the +`segment` index, and must emit the `SseEvent` union: `start` first, +`delta` per text chunk, `usage` when known, then exactly one `done`, with +`seq` counting from 0. A tool call is `tool_call` in three phases, start, +zero or more deltas and end, and a segment that made calls ends with +`done{awaiting_client_tools}`; the loop then runs the handlers and calls +`run` again with the next segment, whose history carries the calls as +`WireToolCallPart`s on the assistant message and the results as +`WireToolResultPart`s on a user message. An `error` event followed by +`done{error}` reports a failure; a stream error with no `done` means the +connection failed. diff --git a/docs/src/content/docs/stacflow/tools.mdx b/docs/src/content/docs/stacflow/tools.mdx new file mode 100644 index 0000000..903173a --- /dev/null +++ b/docs/src/content/docs/stacflow/tools.mdx @@ -0,0 +1,136 @@ +--- +title: Tools +description: Let the model take actions in your app, with a card per call and a confirmation before destructive ones. +sidebar: + order: 3 +--- + +import { Aside } from '@astrojs/starlight/components'; + +A tool is an action the app lets the model take: switch the theme, create +a task, fetch a statement. You register it with a name, a description the +model reads, a JSON Schema for its arguments and the handler that runs +it. The model decides when to call it, the SDK runs the handler, and the +model finishes its reply with the result in hand. + +## Registering a tool + +```dart +final themeMode = ValueNotifier(ThemeMode.system); + +final chat = StacFlowChat( + provider: GeminiProvider(apiKey: key), + tools: [ + Tool( + name: 'set_theme', + description: 'Switches the app between light and dark mode.', + parameters: const { + 'type': 'object', + 'properties': { + 'mode': { + 'type': 'string', + 'enum': ['light', 'dark', 'system'], + 'description': 'The theme to apply.', + }, + }, + 'required': ['mode'], + }, + label: 'Set theme', + detail: (args) => args['mode'] as String?, + run: (call) { + themeMode.value = ThemeMode.values.byName(call.args['mode'] as String); + return {'mode': call.args['mode']}; + }, + ), + ], +); +``` + +`name` is what the model calls: letters, digits, underscores and dashes, +unique within the chat. `description` and `parameters` are prompts in +disguise; write them for the model. `label` titles the card and `detail` +picks the chip beside it from a call's arguments. The handler receives a +`ToolCall` with the arguments and returns any JSON-encodable value. + +## Permission and confirmation + +Every tool carries a permission class, and the class decides whether the +user is asked first: + +| Permission | Runs | +|---|---| +| `read` | Silently. | +| `write` (default) | Without asking, unless the tool sets `confirm: true`. | +| `destructive` | After the user approves the confirmation card, always. | + +The card's question comes from `confirmation`, a function of the +arguments, or `Allow