From 83f843358e5545cd2f675368eed6223fa6fd7e62 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Wed, 15 Jul 2026 00:59:24 -0600 Subject: [PATCH] Adopt Traverse platform demos and starter kit (#151) Move android-demo, browser-consumer, macos-demo, react-demo, and youaskm3-starter-kit into reference-apps with offline smoke gates, shared expedition fixture, and docs that point at public Traverse surfaces only. Co-authored-by: Cursor --- AGENTS.md | 2 +- CLAUDE.md | 8 + README.md | 7 + apps/android-demo/README.md | 27 + apps/android-demo/app/build.gradle.kts | 42 + .../app/src/main/AndroidManifest.xml | 16 + .../assets/expedition-runtime-session.json | 9 + .../com/traverse/demo/android/MainActivity.kt | 99 + .../app/src/main/res/values/strings.xml | 3 + apps/android-demo/build.gradle.kts | 4 + apps/android-demo/gradle.properties | 3 + apps/android-demo/settings.gradle.kts | 2 + apps/browser-consumer/README.md | 24 + apps/browser-consumer/index.js | 67 + apps/browser-consumer/package.json | 7 + apps/macos-demo/Package.swift | 18 + apps/macos-demo/README.md | 27 + .../DemoContentView.swift | 87 + .../TraverseMacOSDemoApp/DemoSession.swift | 136 + .../TraverseMacOSDemoApp.swift | 12 + apps/react-demo/README.md | 41 + apps/react-demo/index.html | 16 + .../public/expedition-runtime-session.json | 76 + apps/react-demo/server.mjs | 157 + apps/react-demo/src/browser-adapter-client.js | 330 + apps/react-demo/src/main.js | 234 + apps/react-demo/src/styles.css | 296 + .../vendor/react-dom.development.js | 29924 ++++++++++++++++ apps/react-demo/vendor/react.development.js | 3343 ++ apps/youaskm3-starter-kit/README.md | 37 + apps/youaskm3-starter-kit/package.json | 7 + docs/adopted-platform-clients.md | 29 + docs/youaskm3-starter-kit.md | 52 + fixtures/expedition-runtime-session.json | 76 + scripts/ci/android_demo_smoke.sh | 29 + scripts/ci/browser_consumer_package_smoke.sh | 115 + scripts/ci/macos_demo_smoke.sh | 30 + scripts/ci/react_demo_live_adapter_smoke.sh | 113 + scripts/ci/react_demo_smoke.sh | 23 + scripts/ci/repository_checks.sh | 17 + scripts/ci/youaskm3_starter_kit_smoke.sh | 22 + 41 files changed, 35566 insertions(+), 1 deletion(-) create mode 100644 apps/android-demo/README.md create mode 100644 apps/android-demo/app/build.gradle.kts create mode 100644 apps/android-demo/app/src/main/AndroidManifest.xml create mode 100644 apps/android-demo/app/src/main/assets/expedition-runtime-session.json create mode 100644 apps/android-demo/app/src/main/java/com/traverse/demo/android/MainActivity.kt create mode 100644 apps/android-demo/app/src/main/res/values/strings.xml create mode 100644 apps/android-demo/build.gradle.kts create mode 100644 apps/android-demo/gradle.properties create mode 100644 apps/android-demo/settings.gradle.kts create mode 100644 apps/browser-consumer/README.md create mode 100644 apps/browser-consumer/index.js create mode 100644 apps/browser-consumer/package.json create mode 100644 apps/macos-demo/Package.swift create mode 100644 apps/macos-demo/README.md create mode 100644 apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoContentView.swift create mode 100644 apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoSession.swift create mode 100644 apps/macos-demo/Sources/TraverseMacOSDemoApp/TraverseMacOSDemoApp.swift create mode 100644 apps/react-demo/README.md create mode 100644 apps/react-demo/index.html create mode 100644 apps/react-demo/public/expedition-runtime-session.json create mode 100644 apps/react-demo/server.mjs create mode 100644 apps/react-demo/src/browser-adapter-client.js create mode 100644 apps/react-demo/src/main.js create mode 100644 apps/react-demo/src/styles.css create mode 100644 apps/react-demo/vendor/react-dom.development.js create mode 100644 apps/react-demo/vendor/react.development.js create mode 100644 apps/youaskm3-starter-kit/README.md create mode 100644 apps/youaskm3-starter-kit/package.json create mode 100644 docs/adopted-platform-clients.md create mode 100644 docs/youaskm3-starter-kit.md create mode 100644 fixtures/expedition-runtime-session.json create mode 100755 scripts/ci/android_demo_smoke.sh create mode 100755 scripts/ci/browser_consumer_package_smoke.sh create mode 100755 scripts/ci/macos_demo_smoke.sh create mode 100755 scripts/ci/react_demo_live_adapter_smoke.sh create mode 100755 scripts/ci/react_demo_smoke.sh create mode 100755 scripts/ci/youaskm3_starter_kit_smoke.sh diff --git a/AGENTS.md b/AGENTS.md index d22eac5..32fc972 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ gh project item-list 2 --owner traverse-framework --format json --limit 300 \ - **Phase 3 embedded runtime** ([#114](https://github.com/traverse-framework/reference-apps/issues/114)–[#116](https://github.com/traverse-framework/reference-apps/issues/116)) — blocked on remaining consumable embedder SDKs: Traverse [#647](https://github.com/traverse-framework/Traverse/issues/647) (Swift), [#648](https://github.com/traverse-framework/Traverse/issues/648) (Kotlin), [#649](https://github.com/traverse-framework/Traverse/issues/649) (.NET). Rust (#650) and Web/TS (#646) embedder SDKs shipped; App-Refs [#117](https://github.com/traverse-framework/reference-apps/issues/117) Linux/CLI and [#113](https://github.com/traverse-framework/reference-apps/issues/113) web-react embed shipped. -Ready: (none — claim from Project 2 when new Ready items appear). +Ready: (none after #151 — claim from Project 2 when new Ready items appear). Update this section when a PR changes platform status (see PR template checklist). diff --git a/CLAUDE.md b/CLAUDE.md index 60918c9..87e2678 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,8 +18,16 @@ apps/ windows-winui/ # WinUI 3 Windows client linux-gtk/ # GTK4 + Rust Linux client cli-rust/ # Rust CLI client + doc-approval/ # doc-approval clients (all platforms) + meeting-notes/ # meeting-notes clients trace-explorer/ web-react/ # Trace Explorer — execution timeline debugger + react-demo/ # Expedition React browser demo (adopted from Traverse) + browser-consumer/ # Browser consumer façade + android-demo/ # Expedition Android demo + macos-demo/ # Expedition macOS demo + youaskm3-starter-kit/ # Downstream browser starter kit +fixtures/ # Shared UI demo fixtures .agents/skills/ app-refs-ops/ # Ops skill for executing Project 2 tickets .specify/memory/ # Constitution and governing principles diff --git a/README.md b/README.md index 2cb6a66..231dbff 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,16 @@ Each app also ships native clients (iOS, macOS, Android, Windows, Linux, CLI) wh | [`apps/doc-approval/`](apps/doc-approval/) | doc-approval clients (all platforms) | | [`apps/meeting-notes/`](apps/meeting-notes/) | meeting-notes clients (web-react shipped) | | [`apps/trace-explorer/web-react/`](apps/trace-explorer/web-react/) | Trace Explorer — execution timeline debugger | +| [`apps/react-demo/`](apps/react-demo/) | Expedition React browser demo (adopted from Traverse) | +| [`apps/browser-consumer/`](apps/browser-consumer/) | Browser consumer façade for downstream apps | +| [`apps/android-demo/`](apps/android-demo/) | Expedition Android demo | +| [`apps/macos-demo/`](apps/macos-demo/) | Expedition macOS demo | +| [`apps/youaskm3-starter-kit/`](apps/youaskm3-starter-kit/) | youaskm3 browser starter kit | +| [`docs/adopted-platform-clients.md`](docs/adopted-platform-clients.md) | Canonical homes for adopted Traverse demos | | [`docs/embedded-runtime-plan.md`](docs/embedded-runtime-plan.md) | Phase 3 target — embedded runtime + multi-capability workflows | | [`docs/traverse-runtime.md`](docs/traverse-runtime.md) | Dev sidecar setup (Phase 1/2) | | [`manifests/traverse-starter/`](manifests/traverse-starter/) | App manifest + component manifests (Phase 2) | +| [`fixtures/`](fixtures/) | Shared UI demo fixtures (e.g. expedition session) | | [`scripts/ci/`](scripts/ci/) | Repository checks, smoke tests, coverage gate | ## Platform clients diff --git a/apps/android-demo/README.md b/apps/android-demo/README.md new file mode 100644 index 0000000..bfa2da9 --- /dev/null +++ b/apps/android-demo/README.md @@ -0,0 +1,27 @@ +# Traverse Android Demo + +Checked-in Android (Compose) demo for Traverse expedition planning. + +Canonical home: **`traverse-framework/reference-apps`** (`apps/android-demo/`). + +What it does: + +- renders one approved expedition flow +- shows ordered runtime state updates +- shows the final trace summary and output panel +- assumes the runtime exists outside the app process + +Fixture-driven rendering uses `app/src/main/assets/expedition-runtime-session.json`. + +## Build (machine with Android SDK) + +```bash +cd apps/android-demo +./gradlew :app:assembleDebug +``` + +## Validation + +```bash +bash scripts/ci/android_demo_smoke.sh +``` diff --git a/apps/android-demo/app/build.gradle.kts b/apps/android-demo/app/build.gradle.kts new file mode 100644 index 0000000..7428ad7 --- /dev/null +++ b/apps/android-demo/app/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.traverse.demo.android" + compileSdk = 35 + + defaultConfig { + applicationId = "com.traverse.demo.android" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "0.1.0" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.14" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation(platform("androidx.compose:compose-bom:2024.09.02")) + implementation("androidx.activity:activity-compose:1.9.2") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") +} diff --git a/apps/android-demo/app/src/main/AndroidManifest.xml b/apps/android-demo/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2084f06 --- /dev/null +++ b/apps/android-demo/app/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/apps/android-demo/app/src/main/assets/expedition-runtime-session.json b/apps/android-demo/app/src/main/assets/expedition-runtime-session.json new file mode 100644 index 0000000..bcee3cc --- /dev/null +++ b/apps/android-demo/app/src/main/assets/expedition-runtime-session.json @@ -0,0 +1,9 @@ +{ + "demo_id": "expedition.plan.v1", + "title": "Plan Expedition", + "request_id": "req-expedition-001", + "execution_id": "exec_req-expedition-001", + "trace_id": "trace_exec_req-expedition-001", + "status": "completed", + "summary": "Traverse evaluates the governed expedition workflow and assembles a final expedition plan." +} diff --git a/apps/android-demo/app/src/main/java/com/traverse/demo/android/MainActivity.kt b/apps/android-demo/app/src/main/java/com/traverse/demo/android/MainActivity.kt new file mode 100644 index 0000000..3759a16 --- /dev/null +++ b/apps/android-demo/app/src/main/java/com/traverse/demo/android/MainActivity.kt @@ -0,0 +1,99 @@ +package com.traverse.demo.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + TraverseAndroidDemoScreen(sampleStateUpdates) + } + } + } +} + +private val sampleStateUpdates = listOf( + DemoStateUpdate( + "Discovering capability", + "Looking up expedition.planning.plan-expedition@1.0.0 in the registry." + ), + DemoStateUpdate( + "Evaluating constraints", + "Confirming the approved expedition capability can run locally." + ), + DemoStateUpdate( + "Executing workflow", + "Traversing the approved expedition planning workflow." + ), + DemoStateUpdate( + "Completed", + "The expedition plan is ready for final review." + ) +) + +data class DemoStateUpdate( + val title: String, + val detail: String, +) + +@Composable +private fun TraverseAndroidDemoScreen(stateUpdates: List) { + Scaffold { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Traverse Android Demo", style = MaterialTheme.typography.headlineSmall) + Text( + "Plan Expedition", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 8.dp) + ) + Text( + "Runtime states and final trace summary render from the approved expedition demo fixture.", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } + + items(stateUpdates) { update -> + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text(update.title, style = MaterialTheme.typography.titleMedium) + Text( + update.detail, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 6.dp) + ) + } + } + } + } + } +} diff --git a/apps/android-demo/app/src/main/res/values/strings.xml b/apps/android-demo/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..75f48c8 --- /dev/null +++ b/apps/android-demo/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Traverse Android Demo + diff --git a/apps/android-demo/build.gradle.kts b/apps/android-demo/build.gradle.kts new file mode 100644 index 0000000..017d909 --- /dev/null +++ b/apps/android-demo/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.5.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.24" apply false +} diff --git a/apps/android-demo/gradle.properties b/apps/android-demo/gradle.properties new file mode 100644 index 0000000..e696167 --- /dev/null +++ b/apps/android-demo/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official diff --git a/apps/android-demo/settings.gradle.kts b/apps/android-demo/settings.gradle.kts new file mode 100644 index 0000000..9614dec --- /dev/null +++ b/apps/android-demo/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "TraverseAndroidDemo" +include(":app") diff --git a/apps/browser-consumer/README.md b/apps/browser-consumer/README.md new file mode 100644 index 0000000..b7ef0b4 --- /dev/null +++ b/apps/browser-consumer/README.md @@ -0,0 +1,24 @@ +# Traverse Browser Consumer Package + +Browser-targeted consumer façade for downstream apps such as `youaskm3`. + +Canonical home: **`traverse-framework/reference-apps`** (`apps/browser-consumer/`). + +It reuses the approved live browser adapter client from [`apps/react-demo/`](../react-demo/) and exposes a browser-safe subscription flow. Runtime ordering, trace visibility, and terminal outcomes come from Traverse public surfaces — not private app logic. + +## Quick Start + +```bash +node -e "const client = require('./apps/browser-consumer'); console.log(client.APPROVED_BROWSER_CONSUMER_SESSION.title)" +``` + +## Validation + +Offline façade load is covered by `bash scripts/ci/youaskm3_starter_kit_smoke.sh`. + +Live adapter path (requires `TRAVERSE_REPO`): + +```bash +export TRAVERSE_REPO=/path/to/Traverse +bash scripts/ci/browser_consumer_package_smoke.sh +``` diff --git a/apps/browser-consumer/index.js b/apps/browser-consumer/index.js new file mode 100644 index 0000000..f2e42e8 --- /dev/null +++ b/apps/browser-consumer/index.js @@ -0,0 +1,67 @@ +(function (root, factory) { + const client = factory(); + + if (typeof module === "object" && module.exports) { + module.exports = client; + } + + if (root) { + root.TraverseBrowserConsumer = client; + } +})(typeof globalThis !== "undefined" ? globalThis : this, function () { + let baseClient = null; + + if (typeof require === "function") { + try { + baseClient = require("../react-demo/src/browser-adapter-client.js"); + } catch { + baseClient = null; + } + } + + if (!baseClient && typeof globalThis !== "undefined" && globalThis.TraverseReactDemoClient) { + baseClient = globalThis.TraverseReactDemoClient; + } + + if (!baseClient) { + throw new Error( + "Traverse browser consumer requires the approved browser adapter client to be available.", + ); + } + + const APPROVED_BROWSER_CONSUMER_SESSION = { + ...baseClient.APPROVED_BROWSER_DEMO_SESSION, + title: "Traverse Browser Consumer", + summary: + "Traverse's browser-targeted consumer facade for downstream browser-hosted apps like youaskm3.", + }; + + function createBrowserConsumerState() { + return baseClient.createLiveDemoState(); + } + + function buildBrowserConsumerSubscriptionRequest() { + return baseClient.buildApprovedSubscriptionRequest(); + } + + function runBrowserConsumerSubscription(options = {}) { + return baseClient.runLiveBrowserSubscription(options); + } + + function applyBrowserConsumerMessage(state, message, created) { + return baseClient.applyBrowserSubscriptionMessage(state, message, created); + } + + function browserConsumerTraceSummary(trace, terminalResult) { + return baseClient.traceSummary(trace, terminalResult); + } + + return { + APPROVED_BROWSER_CONSUMER_SESSION, + applyBrowserConsumerMessage, + browserConsumerTraceSummary, + buildBrowserConsumerSubscriptionRequest, + createBrowserConsumerState, + runBrowserConsumerSubscription, + }; +}); diff --git a/apps/browser-consumer/package.json b/apps/browser-consumer/package.json new file mode 100644 index 0000000..0970132 --- /dev/null +++ b/apps/browser-consumer/package.json @@ -0,0 +1,7 @@ +{ + "name": "traverse-browser-consumer", + "version": "0.1.0", + "private": true, + "main": "index.js", + "description": "Traverse browser-targeted consumer facade for downstream browser-hosted apps." +} diff --git a/apps/macos-demo/Package.swift b/apps/macos-demo/Package.swift new file mode 100644 index 0000000..62193ec --- /dev/null +++ b/apps/macos-demo/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "TraverseMacOSDemo", + platforms: [ + .macOS(.v14), + ], + products: [ + .executable(name: "TraverseMacOSDemoApp", targets: ["TraverseMacOSDemoApp"]), + ], + targets: [ + .executableTarget( + name: "TraverseMacOSDemoApp", + path: "Sources/TraverseMacOSDemoApp" + ), + ] +) diff --git a/apps/macos-demo/README.md b/apps/macos-demo/README.md new file mode 100644 index 0000000..a37f9bb --- /dev/null +++ b/apps/macos-demo/README.md @@ -0,0 +1,27 @@ +# Traverse macOS Demo + +Checked-in native SwiftUI demo for Traverse expedition planning. + +Canonical home: **`traverse-framework/reference-apps`** (`apps/macos-demo/`). + +What it does: + +- renders one approved expedition flow +- shows ordered runtime state updates +- shows the final trace summary and output panel +- keeps the runtime separate from the app process + +Fixture source: [`fixtures/expedition-runtime-session.json`](../../fixtures/expedition-runtime-session.json) (run the app from the repository root so the relative path resolves). + +## Run (full Xcode) + +```bash +open apps/macos-demo/Package.swift +# run TraverseMacOSDemoApp +``` + +## Validation + +```bash +bash scripts/ci/macos_demo_smoke.sh +``` diff --git a/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoContentView.swift b/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoContentView.swift new file mode 100644 index 0000000..a885f3c --- /dev/null +++ b/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoContentView.swift @@ -0,0 +1,87 @@ +import SwiftUI + +struct DemoSessionViewModel { + let session: DemoSession + + static func sample() -> DemoSessionViewModel { + DemoSessionViewModel(session: DemoSessionRepository.sample()) + } +} + +struct DemoContentView: View { + let viewModel: DemoSessionViewModel + + var body: some View { + NavigationSplitView { + List(viewModel.session.stateUpdates) { update in + VStack(alignment: .leading, spacing: 6) { + Text(update.title) + .font(.headline) + Text(update.detail) + .font(.subheadline) + .foregroundStyle(.secondary) + Text(update.timestamp) + .font(.caption.monospaced()) + .foregroundStyle(.tertiary) + } + .padding(.vertical, 6) + } + .navigationTitle(viewModel.session.title) + } detail: { + ScrollView { + VStack(alignment: .leading, spacing: 18) { + GroupBox("Runtime Summary") { + VStack(alignment: .leading, spacing: 10) { + labeledRow("Status", value: viewModel.session.status) + labeledRow("Request", value: viewModel.session.requestID) + labeledRow("Execution", value: viewModel.session.executionID) + labeledRow("Trace", value: viewModel.session.traceID) + labeledRow("Target", value: viewModel.session.trace.placement.selectedTarget) + Text(viewModel.session.summary) + .font(.body) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + GroupBox("Request Goal") { + Text(viewModel.session.request.goal) + .frame(maxWidth: .infinity, alignment: .leading) + } + + GroupBox("Emitted Events") { + VStack(alignment: .leading, spacing: 8) { + ForEach(viewModel.session.trace.emittedEvents, id: \.self) { eventID in + Text(eventID) + .font(.body.monospaced()) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + GroupBox("Final Output") { + VStack(alignment: .leading, spacing: 10) { + labeledRow("Plan ID", value: viewModel.session.trace.output.planID) + labeledRow("Route", value: viewModel.session.trace.output.route) + labeledRow("Weather", value: viewModel.session.trace.output.weatherSummary) + labeledRow("Team Status", value: viewModel.session.trace.output.teamStatus) + labeledRow("Next Action", value: viewModel.session.trace.output.nextAction) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(24) + } + } + } + + @ViewBuilder + private func labeledRow(_ label: String, value: String) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text(label) + .font(.caption) + .foregroundStyle(.secondary) + Text(value) + .font(.body) + } + } +} diff --git a/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoSession.swift b/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoSession.swift new file mode 100644 index 0000000..4e2ef21 --- /dev/null +++ b/apps/macos-demo/Sources/TraverseMacOSDemoApp/DemoSession.swift @@ -0,0 +1,136 @@ +import Foundation + +struct DemoSession: Decodable { + let demoID: String + let title: String + let requestID: String + let executionID: String + let traceID: String + let status: String + let summary: String + let request: DemoRequest + let stateUpdates: [DemoStateUpdate] + let trace: DemoTrace + + enum CodingKeys: String, CodingKey { + case demoID = "demo_id" + case title + case requestID = "request_id" + case executionID = "execution_id" + case traceID = "trace_id" + case status + case summary + case request + case stateUpdates = "state_updates" + case trace + } +} + +struct DemoRequest: Decodable { + let goal: String + let requestedTarget: String + let caller: String + + enum CodingKeys: String, CodingKey { + case goal + case requestedTarget = "requested_target" + case caller + } +} + +struct DemoStateUpdate: Decodable, Identifiable { + let state: String + let title: String + let timestamp: String + let detail: String + + var id: String { "\(timestamp)-\(state)" } +} + +struct DemoTrace: Decodable { + let selectedCapabilityID: String + let selectedCapabilityVersion: String + let placement: DemoPlacement + let emittedEvents: [String] + let output: DemoOutput + + enum CodingKeys: String, CodingKey { + case selectedCapabilityID = "selected_capability_id" + case selectedCapabilityVersion = "selected_capability_version" + case placement + case emittedEvents = "emitted_events" + case output + } +} + +struct DemoPlacement: Decodable { + let requestedTarget: String + let selectedTarget: String + let status: String + let reason: String + + enum CodingKeys: String, CodingKey { + case requestedTarget = "requested_target" + case selectedTarget = "selected_target" + case status + case reason + } +} + +struct DemoOutput: Decodable { + let planID: String + let route: String + let weatherSummary: String + let teamStatus: String + let nextAction: String + + enum CodingKeys: String, CodingKey { + case planID = "plan_id" + case route + case weatherSummary = "weather_summary" + case teamStatus = "team_status" + case nextAction = "next_action" + } +} + +enum DemoSessionRepository { + static func sample() -> DemoSession { + let url = URL(fileURLWithPath: "fixtures/expedition-runtime-session.json") + let data = (try? Data(contentsOf: url)) ?? Data() + let decoder = JSONDecoder() + + if let session = try? decoder.decode(DemoSession.self, from: data) { + return session + } + + return DemoSession( + demoID: "fallback", + title: "Traverse macOS Demo", + requestID: "req-fallback", + executionID: "exec-fallback", + traceID: "trace-fallback", + status: "error", + summary: "Fixture loading failed.", + request: DemoRequest(goal: "Unavailable", requestedTarget: "local", caller: "native_demo"), + stateUpdates: [], + trace: DemoTrace( + selectedCapabilityID: "unavailable", + selectedCapabilityVersion: "0.0.0", + placement: DemoPlacement( + requestedTarget: "local", + selectedTarget: "local", + status: "not_attempted", + reason: "fixture_unavailable" + ), + emittedEvents: [], + output: DemoOutput( + planID: "unavailable", + route: "Unavailable", + weatherSummary: "Unavailable", + teamStatus: "unavailable", + nextAction: "Restore the fixture file." + ) + ) + ) + } +} diff --git a/apps/macos-demo/Sources/TraverseMacOSDemoApp/TraverseMacOSDemoApp.swift b/apps/macos-demo/Sources/TraverseMacOSDemoApp/TraverseMacOSDemoApp.swift new file mode 100644 index 0000000..ad45092 --- /dev/null +++ b/apps/macos-demo/Sources/TraverseMacOSDemoApp/TraverseMacOSDemoApp.swift @@ -0,0 +1,12 @@ +import SwiftUI + +@main +struct TraverseMacOSDemoApp: App { + var body: some Scene { + WindowGroup("Traverse macOS Demo") { + DemoContentView(viewModel: DemoSessionViewModel.sample()) + .frame(minWidth: 1080, minHeight: 720) + } + .windowStyle(.titleBar) + } +} diff --git a/apps/react-demo/README.md b/apps/react-demo/README.md new file mode 100644 index 0000000..8a0a654 --- /dev/null +++ b/apps/react-demo/README.md @@ -0,0 +1,41 @@ +# Traverse React Demo + +Checked-in React browser demo for Traverse expedition planning. + +Canonical home: **`traverse-framework/reference-apps`** (`apps/react-demo/`). + +What it does: + +- renders one approved expedition flow +- allows one approved request submission path +- shows ordered runtime state updates from the live local browser adapter +- shows the final trace snapshot and output panel after the stream completes + +## Local live run + +Requires a Traverse checkout with `traverse-cli`: + +```bash +export TRAVERSE_REPO=/path/to/Traverse +(cd "$TRAVERSE_REPO" && cargo run -p traverse-cli -- browser-adapter serve --bind 127.0.0.1:4174) +node apps/react-demo/server.mjs --adapter http://127.0.0.1:4174 --port 4173 +``` + +Open `http://127.0.0.1:4173`. + +## Fallback preview + +```bash +python3 -m http.server 4173 --directory apps/react-demo +``` + +## Validation + +```bash +bash scripts/ci/react_demo_smoke.sh +# optional live path: +export TRAVERSE_REPO=/path/to/Traverse +bash scripts/ci/react_demo_live_adapter_smoke.sh +``` + +Runtime docs: [browser-adapter.md](https://github.com/traverse-framework/Traverse/blob/main/docs/browser-adapter.md), [quickstart](https://github.com/traverse-framework/Traverse/blob/main/quickstart.md). diff --git a/apps/react-demo/index.html b/apps/react-demo/index.html new file mode 100644 index 0000000..6a7e124 --- /dev/null +++ b/apps/react-demo/index.html @@ -0,0 +1,16 @@ + + + + + + Traverse React Demo + + + +
+ + + + + + diff --git a/apps/react-demo/public/expedition-runtime-session.json b/apps/react-demo/public/expedition-runtime-session.json new file mode 100644 index 0000000..174391a --- /dev/null +++ b/apps/react-demo/public/expedition-runtime-session.json @@ -0,0 +1,76 @@ +{ + "demo_id": "expedition.plan.v1", + "title": "Plan Expedition", + "request_id": "req-expedition-001", + "execution_id": "exec_req-expedition-001", + "trace_id": "trace_exec_req-expedition-001", + "status": "completed", + "summary": "Traverse evaluates the governed expedition workflow and assembles a final expedition plan.", + "request": { + "goal": "Plan a two-day alpine expedition for a four-person team.", + "requested_target": "local", + "caller": "browser_demo" + }, + "state_updates": [ + { + "state": "discovering", + "title": "Discovering capability", + "timestamp": "2026-03-30T10:00:00Z", + "detail": "Looking up expedition.planning.plan-expedition@1.0.0 in the private-first registry." + }, + { + "state": "evaluating_constraints", + "title": "Evaluating constraints", + "timestamp": "2026-03-30T10:00:01Z", + "detail": "Confirmed local execution is allowed for the approved expedition capability." + }, + { + "state": "selecting", + "title": "Selecting workflow-backed capability", + "timestamp": "2026-03-30T10:00:02Z", + "detail": "Selected expedition.planning.plan-expedition@1.0.0." + }, + { + "state": "executing", + "title": "Executing workflow", + "timestamp": "2026-03-30T10:00:03Z", + "detail": "Running the approved workflow steps to capture intent, assess conditions, validate readiness, and assemble the plan." + }, + { + "state": "emitting_events", + "title": "Publishing governed events", + "timestamp": "2026-03-30T10:00:04Z", + "detail": "Emitting expedition objective, interpreted intent, conditions summary, readiness, and assembled plan events." + }, + { + "state": "completed", + "title": "Completed", + "timestamp": "2026-03-30T10:00:05Z", + "detail": "The final expedition plan is ready for review." + } + ], + "trace": { + "selected_capability_id": "expedition.planning.plan-expedition", + "selected_capability_version": "1.0.0", + "placement": { + "requested_target": "local", + "selected_target": "local", + "status": "selected", + "reason": "requested_target_selected" + }, + "emitted_events": [ + "expedition.planning.expedition-objective-captured@1.0.0", + "expedition.planning.expedition-intent-interpreted@1.0.0", + "expedition.planning.conditions-summary-assessed@1.0.0", + "expedition.planning.team-readiness-validated@1.0.0", + "expedition.planning.expedition-plan-assembled@1.0.0" + ], + "output": { + "plan_id": "plan-001", + "route": "North Glacier approach with base-camp ascent", + "weather_summary": "Cold start with stable winds and acceptable avalanche risk by midday.", + "team_status": "ready_with_layering_adjustments", + "next_action": "Review equipment checklist and confirm departure window." + } + } +} diff --git a/apps/react-demo/server.mjs b/apps/react-demo/server.mjs new file mode 100644 index 0000000..b76710d --- /dev/null +++ b/apps/react-demo/server.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join, normalize } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = fileURLToPath(new URL(".", import.meta.url)); +const defaultPort = 4173; +const defaultAdapterBaseUrl = "http://127.0.0.1:4174"; + +const { port, adapterBaseUrl } = parseArgs(process.argv.slice(2)); +const adapterOrigin = new URL(adapterBaseUrl); + +const server = createServer(async (request, response) => { + try { + if (request.url?.startsWith("/local/browser-subscriptions")) { + await proxyBrowserAdapter(request, response, adapterOrigin); + return; + } + + await serveStaticAsset(request, response); + } catch (error) { + response.statusCode = 500; + response.setHeader("Content-Type", "text/plain; charset=utf-8"); + response.end(String(error)); + } +}); + +server.listen(port, "127.0.0.1", () => { + console.log(`Traverse React demo serving on http://127.0.0.1:${port}`); + console.log(`Proxying browser adapter requests to ${adapterOrigin.origin}`); +}); + +function parseArgs(args) { + let port = defaultPort; + let adapterBaseUrl = defaultAdapterBaseUrl; + + for (let index = 0; index < args.length; index += 1) { + const current = args[index]; + if (current === "--port" && args[index + 1]) { + port = Number(args[index + 1]); + index += 1; + } else if (current === "--adapter" && args[index + 1]) { + adapterBaseUrl = args[index + 1]; + index += 1; + } + } + + if (!Number.isInteger(port) || port <= 0) { + throw new Error(`invalid port: ${port}`); + } + + return { port, adapterBaseUrl }; +} + +async function proxyBrowserAdapter(request, response, adapterOrigin) { + const targetUrl = new URL(request.url, adapterOrigin); + const headers = new Headers(); + + for (const [name, value] of Object.entries(request.headers)) { + if (value === undefined) { + continue; + } + if (["host", "connection", "content-length"].includes(name.toLowerCase())) { + continue; + } + headers.set(name, Array.isArray(value) ? value.join(", ") : value); + } + + let body = undefined; + if (!["GET", "HEAD"].includes(request.method || "")) { + body = await readRequestBody(request); + } + + const upstreamResponse = await fetch(targetUrl, { + method: request.method, + headers, + body, + }); + + response.statusCode = upstreamResponse.status; + response.statusMessage = upstreamResponse.statusText; + + upstreamResponse.headers.forEach((value, name) => { + if (name.toLowerCase() === "content-length") { + return; + } + response.setHeader(name, value); + }); + + if (!upstreamResponse.body) { + response.end(); + return; + } + + const reader = upstreamResponse.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + response.write(Buffer.from(value)); + } + + response.end(); +} + +async function serveStaticAsset(request, response) { + const requestPath = request.url?.split("?")[0] ?? "/"; + const normalizedPath = normalize(requestPath).replace(/^([.][.][/\\])+/, ""); + const filePath = normalizedPath === "/" ? "index.html" : normalizedPath.replace(/^\//, ""); + const resolvedPath = join(rootDir, filePath); + + try { + const contents = await readFile(resolvedPath); + response.statusCode = 200; + response.setHeader("Content-Type", contentTypeFor(resolvedPath)); + response.end(contents); + } catch { + if (requestPath !== "/") { + const indexPath = join(rootDir, "index.html"); + const contents = await readFile(indexPath); + response.statusCode = 200; + response.setHeader("Content-Type", "text/html; charset=utf-8"); + response.end(contents); + return; + } + + response.statusCode = 404; + response.setHeader("Content-Type", "text/plain; charset=utf-8"); + response.end("Not found"); + } +} + +function contentTypeFor(filePath) { + switch (extname(filePath)) { + case ".html": + return "text/html; charset=utf-8"; + case ".js": + return "text/javascript; charset=utf-8"; + case ".css": + return "text/css; charset=utf-8"; + case ".json": + return "application/json; charset=utf-8"; + default: + return "application/octet-stream"; + } +} + +async function readRequestBody(request) { + const chunks = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +} diff --git a/apps/react-demo/src/browser-adapter-client.js b/apps/react-demo/src/browser-adapter-client.js new file mode 100644 index 0000000..f59d113 --- /dev/null +++ b/apps/react-demo/src/browser-adapter-client.js @@ -0,0 +1,330 @@ +(function (root, factory) { + const client = factory(); + + if (typeof module === "object" && module.exports) { + module.exports = client; + } + + if (root) { + root.TraverseReactDemoClient = client; + } +})(typeof globalThis !== "undefined" ? globalThis : this, function () { + const APPROVED_BROWSER_DEMO_SESSION = { + title: "Plan Expedition", + summary: + "Traverse evaluates the governed expedition workflow and assembles a final expedition plan.", + request: { + goal: "Plan a two-day alpine expedition for a four-person team.", + requested_target: "local", + caller: "browser_demo", + }, + request_id: "expedition-plan-request-001", + execution_id: "exec_expedition-plan-request-001", + trace_id: "trace_exec_expedition-plan-request-001", + }; + + function createLiveDemoState() { + return { + phase: "idle", + statusLabel: "ready", + streamBanner: "No subscription active yet. Submit the approved request to begin.", + requestId: null, + executionId: null, + stateUpdates: [], + liveTrace: null, + liveResult: null, + error: "", + }; + } + + function buildApprovedSubscriptionRequest() { + return { + subscription_request: { + kind: "browser_runtime_subscription_request", + schema_version: "1.0.0", + governing_spec: "013-browser-runtime-subscription", + request_id: APPROVED_BROWSER_DEMO_SESSION.request_id, + }, + }; + } + + function humanizeName(value) { + return value + .toString() + .split("_") + .filter(Boolean) + .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) + .join(" "); + } + + function formatDetailValue(value) { + if (value === null || value === undefined) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return JSON.stringify(value); + } + + function describeStateEvent(stateEvent) { + const details = stateEvent.details || {}; + const parts = []; + + if (details.transition_reason) { + parts.push(humanizeName(details.transition_reason)); + } + + for (const [key, value] of Object.entries(details)) { + if (key === "transition_reason") { + continue; + } + parts.push(`${humanizeName(key)}: ${formatDetailValue(value)}`); + } + + return parts.length > 0 ? parts.join(" · ") : "State update received."; + } + + function formatStateUpdate(stateEvent) { + return { + state: stateEvent.state, + title: humanizeName(stateEvent.state), + timestamp: stateEvent.entered_at, + detail: describeStateEvent(stateEvent), + }; + } + + function normalizeSubscriptionMessage(message) { + const variant = Object.keys(message || {})[0]; + if (!variant) { + return null; + } + return { + variant, + payload: message[variant], + }; + } + + function parseSubscriptionFrame(frame) { + let eventName = ""; + let data = ""; + + for (const line of frame.split(/\r?\n/)) { + if (line.startsWith("event: ")) { + eventName = line.slice("event: ".length); + } else if (line.startsWith("data: ")) { + data += line.slice("data: ".length); + } + } + + if (!eventName || !data) { + return null; + } + + return { + event: eventName, + data: JSON.parse(data), + }; + } + + function parseSubscriptionFrames(text) { + return text + .split(/\r?\n\r?\n/) + .map((frame) => frame.trim()) + .filter(Boolean) + .map(parseSubscriptionFrame) + .filter(Boolean); + } + + async function runLiveBrowserSubscription({ + baseUrl = "", + fetchImpl = globalThis.fetch, + onMessage, + } = {}) { + const adapterPrefix = baseUrl.replace(/\/$/, ""); + const createResponse = await fetchImpl(`${adapterPrefix}/local/browser-subscriptions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(buildApprovedSubscriptionRequest()), + }); + + const createdPayload = await createResponse.text(); + if (!createResponse.ok) { + throw new Error(`local browser adapter setup failed: ${createdPayload}`); + } + + const created = JSON.parse(createdPayload); + const streamResponse = await fetchImpl(`${adapterPrefix}${created.stream_url}`, { + headers: { + Accept: "text/event-stream", + }, + }); + + if (!streamResponse.ok) { + const errorPayload = await streamResponse.text(); + throw new Error(`local browser adapter stream failed: ${errorPayload}`); + } + + if (!streamResponse.body || typeof streamResponse.body.getReader !== "function") { + throw new Error("local browser adapter stream did not expose a readable body"); + } + + const reader = streamResponse.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + const messages = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + buffer += decoder.decode(value, { stream: true }); + const frames = buffer.split(/\r?\n\r?\n/); + buffer = frames.pop() || ""; + + for (const frame of frames) { + const parsed = parseSubscriptionFrame(frame.trim()); + if (!parsed) { + continue; + } + + const normalized = normalizeSubscriptionMessage(parsed.data); + if (!normalized) { + continue; + } + + messages.push(normalized); + if (typeof onMessage === "function") { + onMessage(normalized, created); + } + } + } + + const tail = buffer.trim(); + if (tail) { + const parsed = parseSubscriptionFrame(tail); + if (parsed) { + const normalized = normalizeSubscriptionMessage(parsed.data); + if (normalized) { + messages.push(normalized); + if (typeof onMessage === "function") { + onMessage(normalized, created); + } + } + } + } + + return { + created, + messages, + }; + } + + function applyBrowserSubscriptionMessage(state, message, created) { + const nextState = { + ...state, + error: "", + }; + + switch (message.variant) { + case "Lifecycle": { + const lifecycle = message.payload; + nextState.requestId = lifecycle.request_id; + nextState.executionId = lifecycle.execution_id; + nextState.statusLabel = + lifecycle.status === "subscription_established" + ? "streaming" + : lifecycle.status === "stream_completed" + ? "completed" + : lifecycle.status; + nextState.streamBanner = + lifecycle.status === "subscription_established" + ? "Subscription established. Streaming ordered runtime updates." + : "Stream completed. Final trace artifact is now visible."; + nextState.phase = + lifecycle.status === "subscription_established" + ? "streaming" + : lifecycle.status === "stream_completed" + ? "completed" + : nextState.phase; + if (created) { + nextState.subscriptionId = created.subscription_id; + } + return nextState; + } + case "State": { + nextState.phase = "streaming"; + nextState.statusLabel = "streaming"; + nextState.streamBanner = "Subscription established. Streaming ordered runtime updates."; + nextState.stateUpdates = nextState.stateUpdates.concat(formatStateUpdate(message.payload.state_event)); + return nextState; + } + case "TraceArtifact": { + nextState.liveTrace = message.payload.trace; + return nextState; + } + case "StreamTerminal": { + nextState.liveResult = message.payload.result; + nextState.phase = "completed"; + nextState.statusLabel = "completed"; + nextState.streamBanner = "Stream completed. Final trace artifact is now visible."; + return nextState; + } + case "Error": { + nextState.phase = "error"; + nextState.statusLabel = "error"; + nextState.error = message.payload.message; + nextState.streamBanner = message.payload.message; + return nextState; + } + default: + return nextState; + } + } + + function traceSummary(trace, terminalResult) { + if (!trace || !trace.selection || !trace.execution) { + return null; + } + + const output = (terminalResult && terminalResult.output) || (trace.result && trace.result.output) || null; + return { + selection: { + capability: trace.selection.selected_capability_id, + version: trace.selection.selected_capability_version, + placementTarget: trace.execution.placement.selected_target, + placementReason: trace.execution.placement.reason, + }, + emittedEvents: trace.emitted_events || [], + output: output + ? { + planId: output.plan_id, + route: output.route, + weatherSummary: output.weather_summary, + teamStatus: output.team_status, + nextAction: output.next_action, + } + : null, + }; + } + + return { + APPROVED_BROWSER_DEMO_SESSION, + applyBrowserSubscriptionMessage, + buildApprovedSubscriptionRequest, + createLiveDemoState, + humanizeName, + normalizeSubscriptionMessage, + parseSubscriptionFrames, + parseSubscriptionFrame, + runLiveBrowserSubscription, + traceSummary, + }; +}); diff --git a/apps/react-demo/src/main.js b/apps/react-demo/src/main.js new file mode 100644 index 0000000..ca24e2b --- /dev/null +++ b/apps/react-demo/src/main.js @@ -0,0 +1,234 @@ +const { createElement: h, useState } = React; + +const DemoClient = window.TraverseReactDemoClient; +const APPROVED_SESSION = DemoClient.APPROVED_BROWSER_DEMO_SESSION; + +function labeledCard(label, value) { + return h( + "div", + null, + h("dt", null, label), + h("dd", null, value), + ); +} + +function timelineItem(update) { + return h( + "li", + { + key: `${update.timestamp}-${update.state}`, + className: "timeline-item", + }, + h("div", { className: "timeline-marker" }, update.state.slice(0, 1).toUpperCase()), + h( + "div", + { className: "timeline-body" }, + h( + "div", + { className: "timeline-topline" }, + h("strong", null, update.title), + h("span", null, update.timestamp), + ), + h("p", null, update.detail), + ), + ); +} + +function liveTraceSection(trace, terminalResult) { + const summary = DemoClient.traceSummary(trace, terminalResult); + + if (!summary) { + return h( + "div", + { className: "trace-placeholder" }, + "Terminal trace is withheld until the ordered state stream reaches completion.", + ); + } + + return [ + h( + "div", + { className: "trace-block", key: "selection" }, + h("h3", null, "Selection"), + h( + "dl", + { className: "trace-list" }, + labeledCard("Capability", summary.selection.capability), + labeledCard("Version", summary.selection.version), + labeledCard( + "Placement", + `${summary.selection.placementTarget} · ${summary.selection.placementReason}`, + ), + ), + ), + h( + "div", + { className: "trace-block", key: "events" }, + h("h3", null, "Emitted Events"), + h( + "ul", + { className: "event-list" }, + summary.emittedEvents.map((eventId) => h("li", { key: eventId }, eventId)), + ), + ), + h( + "div", + { className: "trace-block", key: "output" }, + h("h3", null, "Output"), + h( + "dl", + { className: "trace-list" }, + labeledCard("Plan", summary.output.planId), + labeledCard("Route", summary.output.route), + labeledCard("Weather", summary.output.weatherSummary), + labeledCard("Team Status", summary.output.teamStatus), + labeledCard("Next Action", summary.output.nextAction), + ), + ), + ]; +} + +function DemoApp() { + const [sessionState, setSessionState] = useState(() => DemoClient.createLiveDemoState()); + const [error, setError] = useState(""); + + async function handleSubmitRequest() { + if (sessionState.phase === "streaming") { + return; + } + + setError(""); + setSessionState(DemoClient.createLiveDemoState()); + + try { + await DemoClient.runLiveBrowserSubscription({ + onMessage: (message, created) => { + setSessionState((current) => DemoClient.applyBrowserSubscriptionMessage(current, message, created)); + }, + }); + } catch (reason) { + const message = String(reason); + setError(message); + setSessionState((current) => ({ + ...current, + phase: "error", + statusLabel: "error", + streamBanner: message, + error: message, + })); + } + } + + const statusLabel = sessionState.statusLabel; + const hasTerminalTrace = sessionState.phase === "completed" && sessionState.liveTrace; + const isStreaming = sessionState.phase === "streaming"; + const visibleUpdates = sessionState.stateUpdates; + + if (error) { + return h( + "main", + { className: "page" }, + h( + "section", + { className: "hero" }, + h( + "div", + { className: "hero-copy" }, + h("p", { className: "eyebrow" }, "Traverse Browser Runtime"), + h("h1", null, "Live adapter connection failed."), + h("p", { className: "lede" }, error), + h( + "p", + { className: "lede" }, + "Run the local browser adapter proxy again, or use the documented fixture preview fallback.", + ), + ), + ), + ); + } + + return h( + "main", + { className: "page" }, + h( + "section", + { className: "hero" }, + h( + "div", + { className: "hero-copy" }, + h("p", { className: "eyebrow" }, "Traverse Browser Runtime"), + h("h1", null, APPROVED_SESSION.title), + h("p", { className: "lede" }, APPROVED_SESSION.summary), + h( + "dl", + { className: "request-meta" }, + labeledCard("Goal", APPROVED_SESSION.request.goal), + labeledCard("Target", APPROVED_SESSION.request.requested_target), + labeledCard("Trace", APPROVED_SESSION.trace_id), + labeledCard("Request", APPROVED_SESSION.request_id), + ), + ), + h("div", { className: "status-pill" }, statusLabel), + ), + h( + "section", + { className: "grid" }, + h( + "article", + { className: "panel" }, + h( + "div", + { className: "panel-header" }, + h("h2", null, "Request And Stream"), + h( + "p", + null, + "Submit the approved expedition request, then watch the governed browser subscription stream unfold in order.", + ), + ), + h( + "div", + { className: "request-card" }, + h( + "div", + null, + h("p", { className: "request-label" }, "Approved request"), + h("h3", null, APPROVED_SESSION.title), + h("p", null, APPROVED_SESSION.request.goal), + ), + h( + "button", + { + className: "request-button", + type: "button", + onClick: handleSubmitRequest, + disabled: isStreaming, + }, + isStreaming ? "Streaming approved request..." : "Submit approved request", + ), + ), + h("div", { className: "stream-banner" }, sessionState.streamBanner), + h("ol", { className: "timeline" }, visibleUpdates.map(timelineItem)), + ), + h( + "article", + { className: "panel trace-panel" }, + h( + "div", + { className: "panel-header" }, + h("h2", null, "Terminal Trace"), + h("p", null, "The final governed selection, placement, and output snapshot."), + ), + hasTerminalTrace + ? liveTraceSection(sessionState.liveTrace, sessionState.liveResult) + : h( + "div", + { className: "trace-placeholder" }, + "Terminal trace is withheld until the ordered state stream reaches completion.", + ), + ), + ), + ); +} + +ReactDOM.createRoot(document.getElementById("root")).render(h(DemoApp)); diff --git a/apps/react-demo/src/styles.css b/apps/react-demo/src/styles.css new file mode 100644 index 0000000..c130473 --- /dev/null +++ b/apps/react-demo/src/styles.css @@ -0,0 +1,296 @@ +:root { + color-scheme: light; + --bg: #f7f0e6; + --paper: rgba(255, 250, 243, 0.82); + --ink: #1f1a17; + --muted: #6d625a; + --accent: #145b73; + --accent-soft: #d8eef4; + --line: rgba(31, 26, 23, 0.12); + --shadow: 0 30px 60px rgba(39, 28, 20, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: "Avenir Next", "Segoe UI", sans-serif; + color: var(--ink); + background: + radial-gradient(circle at top left, rgba(20, 91, 115, 0.18), transparent 30%), + radial-gradient(circle at top right, rgba(193, 119, 61, 0.14), transparent 28%), + linear-gradient(180deg, #fffaf4 0%, var(--bg) 100%); +} + +.page { + max-width: 1180px; + margin: 0 auto; + padding: 48px 20px 64px; +} + +.hero { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: flex-start; + margin-bottom: 28px; + padding: 32px; + border: 1px solid var(--line); + border-radius: 28px; + background: var(--paper); + backdrop-filter: blur(14px); + box-shadow: var(--shadow); +} + +.hero h1 { + margin: 8px 0 12px; + font-size: clamp(2.4rem, 6vw, 4.8rem); + line-height: 0.98; + letter-spacing: -0.05em; +} + +.eyebrow { + margin: 0; + font-size: 0.82rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--accent); + font-weight: 700; +} + +.lede { + max-width: 60ch; + margin: 0; + color: var(--muted); + font-size: 1.05rem; + line-height: 1.55; +} + +.status-pill { + flex-shrink: 0; + padding: 10px 14px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.76rem; + font-weight: 700; +} + +.request-meta, +.trace-list { + display: grid; + gap: 12px; + margin: 20px 0 0; +} + +.request-meta { + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); +} + +.request-meta div, +.trace-list div { + padding: 12px 14px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid var(--line); +} + +dt { + margin-bottom: 6px; + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +dd { + margin: 0; + line-height: 1.45; +} + +.grid { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 22px; +} + +.panel { + padding: 24px; + border-radius: 28px; + border: 1px solid var(--line); + background: rgba(255, 253, 249, 0.9); + box-shadow: var(--shadow); +} + +.panel-header h2, +.trace-block h3 { + margin: 0 0 8px; +} + +.panel-header p { + margin: 0 0 18px; + color: var(--muted); +} + +.request-card, +.stream-banner, +.trace-placeholder { + border: 1px solid var(--line); + background: white; +} + +.request-card { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; + margin-bottom: 18px; + padding: 18px 20px; + border-radius: 22px; +} + +.request-label { + margin: 0 0 8px; + color: var(--accent); + font-size: 0.8rem; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 700; +} + +.request-card h3, +.request-card p { + margin: 0; +} + +.request-card p:last-child { + margin-top: 8px; + color: var(--muted); + line-height: 1.45; +} + +.request-button { + border: 0; + border-radius: 999px; + padding: 14px 20px; + background: var(--accent); + color: white; + font: inherit; + font-weight: 700; + cursor: pointer; + box-shadow: 0 14px 28px rgba(20, 91, 115, 0.18); +} + +.request-button:disabled { + cursor: wait; + opacity: 0.72; +} + +.stream-banner, +.trace-placeholder { + margin-bottom: 18px; + padding: 14px 16px; + border-radius: 18px; + color: var(--muted); + line-height: 1.45; +} + +.timeline { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 14px; +} + +.timeline-item { + display: grid; + grid-template-columns: 44px 1fr; + gap: 14px; + align-items: flex-start; +} + +.timeline-marker { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border-radius: 14px; + background: var(--accent); + color: white; + font-weight: 700; +} + +.timeline-body { + padding: 14px 16px; + border-radius: 18px; + background: white; + border: 1px solid var(--line); +} + +.timeline-topline { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: baseline; +} + +.timeline-topline span { + color: var(--muted); + font-size: 0.84rem; +} + +.timeline-body p { + margin: 10px 0 0; + color: var(--muted); + line-height: 1.45; +} + +.trace-panel { + display: grid; + gap: 18px; +} + +.trace-block { + padding: 16px 18px; + border-radius: 20px; + background: white; + border: 1px solid var(--line); +} + +.event-list { + margin: 0; + padding-left: 18px; + display: grid; + gap: 8px; +} + +.event-list li { + color: var(--muted); + line-height: 1.45; +} + +@media (max-width: 900px) { + .hero, + .grid { + grid-template-columns: 1fr; + } + + .hero { + flex-direction: column; + } + + .timeline-topline { + flex-direction: column; + } + + .request-card { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/apps/react-demo/vendor/react-dom.development.js b/apps/react-demo/vendor/react-dom.development.js new file mode 100644 index 0000000..57a309c --- /dev/null +++ b/apps/react-demo/vendor/react-dom.development.js @@ -0,0 +1,29924 @@ +/** + * @license React + * react-dom.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : + typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : + (global = global || self, factory(global.ReactDOM = {}, global.React)); +}(this, (function (exports, React) { 'use strict'; + + var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + + var suppressWarning = false; + function setSuppressWarning(newSuppressWarning) { + { + suppressWarning = newSuppressWarning; + } + } // In DEV, calls to console.warn and console.error get replaced + // by calls to these methods by a Babel plugin. + // + // In PROD (or in packages without access to React internals), + // they are left as they are instead. + + function warn(format) { + { + if (!suppressWarning) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + printWarning('warn', format, args); + } + } + } + function error(format) { + { + if (!suppressWarning) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + + printWarning('error', format, args); + } + } + } + + function printWarning(level, format, args) { + // When changing this logic, you might want to also + // update consoleWithStackDev.www.js as well. + { + var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + var stack = ReactDebugCurrentFrame.getStackAddendum(); + + if (stack !== '') { + format += '%s'; + args = args.concat([stack]); + } // eslint-disable-next-line react-internal/safe-string-coercion + + + var argsWithFormat = args.map(function (item) { + return String(item); + }); // Careful: RN currently depends on this prefix + + argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it + // breaks IE9: https://github.com/facebook/react/issues/13610 + // eslint-disable-next-line react-internal/no-production-logging + + Function.prototype.apply.call(console[level], console, argsWithFormat); + } + } + + var FunctionComponent = 0; + var ClassComponent = 1; + var IndeterminateComponent = 2; // Before we know whether it is function or class + + var HostRoot = 3; // Root of a host tree. Could be nested inside another node. + + var HostPortal = 4; // A subtree. Could be an entry point to a different renderer. + + var HostComponent = 5; + var HostText = 6; + var Fragment = 7; + var Mode = 8; + var ContextConsumer = 9; + var ContextProvider = 10; + var ForwardRef = 11; + var Profiler = 12; + var SuspenseComponent = 13; + var MemoComponent = 14; + var SimpleMemoComponent = 15; + var LazyComponent = 16; + var IncompleteClassComponent = 17; + var DehydratedFragment = 18; + var SuspenseListComponent = 19; + var ScopeComponent = 21; + var OffscreenComponent = 22; + var LegacyHiddenComponent = 23; + var CacheComponent = 24; + var TracingMarkerComponent = 25; + + // ----------------------------------------------------------------------------- + + var enableClientRenderFallbackOnTextMismatch = true; // TODO: Need to review this code one more time before landing + // the react-reconciler package. + + var enableNewReconciler = false; // Support legacy Primer support on internal FB www + + var enableLazyContextPropagation = false; // FB-only usage. The new API has different semantics. + + var enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber + + var enableSuspenseAvoidThisFallback = false; // Enables unstable_avoidThisFallback feature in Fizz + // React DOM Chopping Block + // + // Similar to main Chopping Block but only flags related to React DOM. These are + // grouped because we will likely batch all of them into a single major release. + // ----------------------------------------------------------------------------- + // Disable support for comment nodes as React DOM containers. Already disabled + // in open source, but www codebase still relies on it. Need to remove. + + var disableCommentsAsDOMContainers = true; // Disable javascript: URL strings in href for XSS protection. + // and client rendering, mostly to allow JSX attributes to apply to the custom + // element's object properties instead of only HTML attributes. + // https://github.com/facebook/react/issues/11347 + + var enableCustomElementPropertySupport = false; // Disables children for