From 6af426e57c17fb5953607051edb4cba313a2a441 Mon Sep 17 00:00:00 2001 From: CxAI Ops Date: Sat, 16 May 2026 10:52:04 -0500 Subject: [PATCH] chore: initial commit (Phase 3 scaffold) --- .editorconfig | 15 + .gitea/workflows/ci.yml | 36 ++ .github/workflows/ci.yml | 27 + .gitignore | 54 ++ CHANGELOG.md | 16 + CODEOWNERS | 6 + CONTRIBUTING.md | 23 + CxLLM-IOS.xcodeproj/project.pbxproj | 600 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + CxLLM-IOS/Assets.xcassets/Contents.json | 6 + CxLLM-IOS/ContentView.swift | 61 ++ CxLLM-IOS/CxLLM_IOS.entitlements | 14 + CxLLM-IOS/CxLLM_IOSApp.swift | 32 + CxLLM-IOS/Info.plist | 10 + CxLLM-IOS/Item.swift | 18 + CxLLM-IOSTests/CxLLM_IOSTests.swift | 17 + CxLLM-IOSUITests/CxLLM_IOSUITests.swift | 41 ++ .../CxLLM_IOSUITestsLaunchTests.swift | 33 + CxLLMStudio/ContentView.swift | 67 ++ CxLLMStudio/CxLLMStudioApp.swift | 50 ++ CxLLMStudio/Services/AWSService.swift | 95 +++ CxLLMStudio/Services/AgentService.swift | 181 ++++++ CxLLMStudio/Services/AppState.swift | 77 +++ CxLLMStudio/Services/GatewayService.swift | 48 ++ CxLLMStudio/Services/GitService.swift | 102 +++ CxLLMStudio/Services/MCPService.swift | 214 +++++++ CxLLMStudio/Views/AWS/AWSView.swift | 132 ++++ CxLLMStudio/Views/Agent/AgentView.swift | 331 ++++++++++ CxLLMStudio/Views/Chat/ChatView.swift | 175 +++++ CxLLMStudio/Views/Git/GitView.swift | 189 ++++++ CxLLMStudio/Views/MCP/MCPView.swift | 385 +++++++++++ CxLLMStudio/Views/Models/ModelsView.swift | 179 ++++++ CxLLMStudio/Views/Settings/SettingsView.swift | 90 +++ LICENSE | 21 + Makefile | 43 ++ Package.swift | 23 + README.md | 53 ++ SECURITY.md | 19 + docs/ARCHITECTURE.md | 24 + .../adr/0001-record-architecture-decisions.md | 13 + 42 files changed, 3573 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitea/workflows/ci.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 CxLLM-IOS.xcodeproj/project.pbxproj create mode 100644 CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 CxLLM-IOS/Assets.xcassets/Contents.json create mode 100644 CxLLM-IOS/ContentView.swift create mode 100644 CxLLM-IOS/CxLLM_IOS.entitlements create mode 100644 CxLLM-IOS/CxLLM_IOSApp.swift create mode 100644 CxLLM-IOS/Info.plist create mode 100644 CxLLM-IOS/Item.swift create mode 100644 CxLLM-IOSTests/CxLLM_IOSTests.swift create mode 100644 CxLLM-IOSUITests/CxLLM_IOSUITests.swift create mode 100644 CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift create mode 100644 CxLLMStudio/ContentView.swift create mode 100644 CxLLMStudio/CxLLMStudioApp.swift create mode 100644 CxLLMStudio/Services/AWSService.swift create mode 100644 CxLLMStudio/Services/AgentService.swift create mode 100644 CxLLMStudio/Services/AppState.swift create mode 100644 CxLLMStudio/Services/GatewayService.swift create mode 100644 CxLLMStudio/Services/GitService.swift create mode 100644 CxLLMStudio/Services/MCPService.swift create mode 100644 CxLLMStudio/Views/AWS/AWSView.swift create mode 100644 CxLLMStudio/Views/Agent/AgentView.swift create mode 100644 CxLLMStudio/Views/Chat/ChatView.swift create mode 100644 CxLLMStudio/Views/Git/GitView.swift create mode 100644 CxLLMStudio/Views/MCP/MCPView.swift create mode 100644 CxLLMStudio/Views/Models/ModelsView.swift create mode 100644 CxLLMStudio/Views/Settings/SettingsView.swift create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 Package.swift create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/adr/0001-record-architecture-decisions.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5ef197b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json,md}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..2a804da --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,36 @@ +name: ci +on: + push: + branches: [main, master] + pull_request: {} + workflow_dispatch: {} + +# We run a Linux runner; macOS xcodebuild is not available here. +# This workflow validates structure (yaml, file presence) and basic Swift. +jobs: + validate: + runs-on: cxai-hostinger + container: debian:bookworm + steps: + - name: Install deps + run: | + apt-get update -qq + apt-get install -y --no-install-recommends git ca-certificates curl python3-yaml >/dev/null + - uses: actions/checkout@v4 + - name: Lint workflow YAML + run: | + python3 - <<'PY' + import sys, yaml, glob + for f in glob.glob('.gitea/workflows/*.yml') + glob.glob('.github/workflows/*.yml'): + try: + yaml.safe_load(open(f)) + except Exception as e: + print(f'YAML ERROR {f}: {e}'); sys.exit(1) + print('workflows ok') + PY + - name: Project sanity + run: | + test -f README.md || (echo "missing README" && exit 1) + test -f Makefile || (echo "missing Makefile" && exit 1) + ls *.xcodeproj >/dev/null 2>&1 && echo "xcodeproj: $(ls -d *.xcodeproj)" || true + [ -f Package.swift ] && echo "Package.swift present" || true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..39d201f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-14] + steps: + - uses: actions/checkout@v4 + - name: Show toolchain + run: | + xcodebuild -version || true + swift --version || true + - name: Build + run: make build + - name: Test + run: make test + - name: Lint + run: make lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4ca831 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon? + +# Xcode +build/ +DerivedData/ +*.xcworkspace/xcuserdata/ +*.xcodeproj/xcuserdata/ +*.xcodeproj/project.xcworkspace/xcuserdata/ +*.xcuserstate +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +# SwiftPM +.build/ +.swiftpm/xcode/ +.swiftpm/configuration/ +Package.resolved + +# CocoaPods / Carthage +Pods/ +Carthage/Build/ + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output/ + +# Coverage +*.gcno +*.gcda +*.profdata +*.profraw +coverage/ + +# Editors +.vscode/ +.idea/ +*.swp + +# Local +*.local +secrets.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8bf5a66 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented here. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added +- Repository scaffolding: README, LICENSE (MIT), .gitignore, Makefile, + CONTRIBUTING, SECURITY, CODEOWNERS, .editorconfig, CI workflow. + +## [0.1.0] - 2026-04-22 + +### Added +- Initial app-ios scaffold for CxLLM-IOS. diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..7e6dfca --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# Default owners for everything in this repo. +* @CxAI-LLM/maintainers + +# Build & CI +/.github/ @CxAI-LLM/devops +/Makefile @CxAI-LLM/devops diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c83b2a3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing to CxLLM-IOS + +Thanks for taking the time to contribute! + +## Workflow + +1. Open an issue describing the change before sending a PR for non-trivial work. +2. Fork / branch off `main`. Use a descriptive branch name (`feat/...`, `fix/...`). +3. Keep commits scoped and use **Conventional Commits** (`feat:`, `fix:`, + `docs:`, `refactor:`, `test:`, `chore:`). +4. Run `make lint` and `make test` before pushing. +5. Open a PR — CI must pass before review. + +## Coding style + +- Swift: `swiftformat` defaults + `swiftlint` rules from the umbrella repo. +- Objective-C / C / C++: clang-format `-style=Google`. +- No tabs in Swift; 4-space indent in C/C++. + +## Code of conduct + +By contributing you agree to abide by the project's Code of Conduct +(see the umbrella `cxllm-code` repo). diff --git a/CxLLM-IOS.xcodeproj/project.pbxproj b/CxLLM-IOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000..31bfb58 --- /dev/null +++ b/CxLLM-IOS.xcodeproj/project.pbxproj @@ -0,0 +1,600 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 20F2AF332F99700100E7D2D9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 20F2AF192F99700000E7D2D9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 20F2AF202F99700000E7D2D9; + remoteInfo = "CxLLM-IOS"; + }; + 20F2AF3D2F99700100E7D2D9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 20F2AF192F99700000E7D2D9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 20F2AF202F99700000E7D2D9; + remoteInfo = "CxLLM-IOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 20F2AF212F99700000E7D2D9 /* CxLLM-IOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CxLLM-IOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 20F2AF322F99700100E7D2D9 /* CxLLM-IOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CxLLM-IOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 20F2AF3C2F99700100E7D2D9 /* CxLLM-IOSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CxLLM-IOSUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 20F2AF442F99700100E7D2D9 /* Exceptions for "CxLLM-IOS" folder in "CxLLM-IOS" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 20F2AF202F99700000E7D2D9 /* CxLLM-IOS */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 20F2AF232F99700000E7D2D9 /* CxLLM-IOS */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 20F2AF442F99700100E7D2D9 /* Exceptions for "CxLLM-IOS" folder in "CxLLM-IOS" target */, + ); + path = "CxLLM-IOS"; + sourceTree = ""; + }; + 20F2AF352F99700100E7D2D9 /* CxLLM-IOSTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "CxLLM-IOSTests"; + sourceTree = ""; + }; + 20F2AF3F2F99700100E7D2D9 /* CxLLM-IOSUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "CxLLM-IOSUITests"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 20F2AF1E2F99700000E7D2D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF2F2F99700100E7D2D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF392F99700100E7D2D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 20F2AF182F99700000E7D2D9 = { + isa = PBXGroup; + children = ( + 20F2AF232F99700000E7D2D9 /* CxLLM-IOS */, + 20F2AF352F99700100E7D2D9 /* CxLLM-IOSTests */, + 20F2AF3F2F99700100E7D2D9 /* CxLLM-IOSUITests */, + 20F2AF222F99700000E7D2D9 /* Products */, + ); + sourceTree = ""; + }; + 20F2AF222F99700000E7D2D9 /* Products */ = { + isa = PBXGroup; + children = ( + 20F2AF212F99700000E7D2D9 /* CxLLM-IOS.app */, + 20F2AF322F99700100E7D2D9 /* CxLLM-IOSTests.xctest */, + 20F2AF3C2F99700100E7D2D9 /* CxLLM-IOSUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 20F2AF202F99700000E7D2D9 /* CxLLM-IOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 20F2AF452F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOS" */; + buildPhases = ( + 20F2AF1D2F99700000E7D2D9 /* Sources */, + 20F2AF1E2F99700000E7D2D9 /* Frameworks */, + 20F2AF1F2F99700000E7D2D9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 20F2AF232F99700000E7D2D9 /* CxLLM-IOS */, + ); + name = "CxLLM-IOS"; + packageProductDependencies = ( + ); + productName = "CxLLM-IOS"; + productReference = 20F2AF212F99700000E7D2D9 /* CxLLM-IOS.app */; + productType = "com.apple.product-type.application"; + }; + 20F2AF312F99700100E7D2D9 /* CxLLM-IOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 20F2AF4A2F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOSTests" */; + buildPhases = ( + 20F2AF2E2F99700100E7D2D9 /* Sources */, + 20F2AF2F2F99700100E7D2D9 /* Frameworks */, + 20F2AF302F99700100E7D2D9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 20F2AF342F99700100E7D2D9 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 20F2AF352F99700100E7D2D9 /* CxLLM-IOSTests */, + ); + name = "CxLLM-IOSTests"; + packageProductDependencies = ( + ); + productName = "CxLLM-IOSTests"; + productReference = 20F2AF322F99700100E7D2D9 /* CxLLM-IOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 20F2AF3B2F99700100E7D2D9 /* CxLLM-IOSUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 20F2AF4D2F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOSUITests" */; + buildPhases = ( + 20F2AF382F99700100E7D2D9 /* Sources */, + 20F2AF392F99700100E7D2D9 /* Frameworks */, + 20F2AF3A2F99700100E7D2D9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 20F2AF3E2F99700100E7D2D9 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 20F2AF3F2F99700100E7D2D9 /* CxLLM-IOSUITests */, + ); + name = "CxLLM-IOSUITests"; + packageProductDependencies = ( + ); + productName = "CxLLM-IOSUITests"; + productReference = 20F2AF3C2F99700100E7D2D9 /* CxLLM-IOSUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 20F2AF192F99700000E7D2D9 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + 20F2AF202F99700000E7D2D9 = { + CreatedOnToolsVersion = 26.3; + }; + 20F2AF312F99700100E7D2D9 = { + CreatedOnToolsVersion = 26.3; + TestTargetID = 20F2AF202F99700000E7D2D9; + }; + 20F2AF3B2F99700100E7D2D9 = { + CreatedOnToolsVersion = 26.3; + TestTargetID = 20F2AF202F99700000E7D2D9; + }; + }; + }; + buildConfigurationList = 20F2AF1C2F99700000E7D2D9 /* Build configuration list for PBXProject "CxLLM-IOS" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 20F2AF182F99700000E7D2D9; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 20F2AF222F99700000E7D2D9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 20F2AF202F99700000E7D2D9 /* CxLLM-IOS */, + 20F2AF312F99700100E7D2D9 /* CxLLM-IOSTests */, + 20F2AF3B2F99700100E7D2D9 /* CxLLM-IOSUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 20F2AF1F2F99700000E7D2D9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF302F99700100E7D2D9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF3A2F99700100E7D2D9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 20F2AF1D2F99700000E7D2D9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF2E2F99700100E7D2D9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 20F2AF382F99700100E7D2D9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 20F2AF342F99700100E7D2D9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 20F2AF202F99700000E7D2D9 /* CxLLM-IOS */; + targetProxy = 20F2AF332F99700100E7D2D9 /* PBXContainerItemProxy */; + }; + 20F2AF3E2F99700100E7D2D9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 20F2AF202F99700000E7D2D9 /* CxLLM-IOS */; + targetProxy = 20F2AF3D2F99700100E7D2D9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 20F2AF462F99700100E7D2D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "CxLLM-IOS/CxLLM_IOS.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "CxLLM-IOS/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 20F2AF472F99700100E7D2D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "CxLLM-IOS/CxLLM_IOS.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "CxLLM-IOS/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 20F2AF482F99700100E7D2D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = DKWVC9FQJY; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 20F2AF492F99700100E7D2D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = DKWVC9FQJY; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 20F2AF4B2F99700100E7D2D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOSTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CxLLM-IOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CxLLM-IOS"; + }; + name = Debug; + }; + 20F2AF4C2F99700100E7D2D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOSTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CxLLM-IOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CxLLM-IOS"; + }; + name = Release; + }; + 20F2AF4E2F99700100E7D2D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOSUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "CxLLM-IOS"; + }; + name = Debug; + }; + 20F2AF4F2F99700100E7D2D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DKWVC9FQJY; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "cxai-studio.CxLLM-IOSUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "CxLLM-IOS"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 20F2AF1C2F99700000E7D2D9 /* Build configuration list for PBXProject "CxLLM-IOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 20F2AF482F99700100E7D2D9 /* Debug */, + 20F2AF492F99700100E7D2D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 20F2AF452F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 20F2AF462F99700100E7D2D9 /* Debug */, + 20F2AF472F99700100E7D2D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 20F2AF4A2F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 20F2AF4B2F99700100E7D2D9 /* Debug */, + 20F2AF4C2F99700100E7D2D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 20F2AF4D2F99700100E7D2D9 /* Build configuration list for PBXNativeTarget "CxLLM-IOSUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 20F2AF4E2F99700100E7D2D9 /* Debug */, + 20F2AF4F2F99700100E7D2D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 20F2AF192F99700000E7D2D9 /* Project object */; +} diff --git a/CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json b/CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CxLLM-IOS/Assets.xcassets/Contents.json b/CxLLM-IOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/CxLLM-IOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CxLLM-IOS/ContentView.swift b/CxLLM-IOS/ContentView.swift new file mode 100644 index 0000000..23fa8b9 --- /dev/null +++ b/CxLLM-IOS/ContentView.swift @@ -0,0 +1,61 @@ +// +// ContentView.swift +// CxLLM-IOS +// +// Created by Stephen Carter on 4/22/26. +// + +import SwiftUI +import SwiftData + +struct ContentView: View { + @Environment(\.modelContext) private var modelContext + @Query private var items: [Item] + + var body: some View { + NavigationSplitView { + List { + ForEach(items) { item in + NavigationLink { + Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") + } label: { + Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) + } + } + .onDelete(perform: deleteItems) + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } + ToolbarItem { + Button(action: addItem) { + Label("Add Item", systemImage: "plus") + } + } + } + } detail: { + Text("Select an item") + } + } + + private func addItem() { + withAnimation { + let newItem = Item(timestamp: Date()) + modelContext.insert(newItem) + } + } + + private func deleteItems(offsets: IndexSet) { + withAnimation { + for index in offsets { + modelContext.delete(items[index]) + } + } + } +} + +#Preview { + ContentView() + .modelContainer(for: Item.self, inMemory: true) +} diff --git a/CxLLM-IOS/CxLLM_IOS.entitlements b/CxLLM-IOS/CxLLM_IOS.entitlements new file mode 100644 index 0000000..9e0940e --- /dev/null +++ b/CxLLM-IOS/CxLLM_IOS.entitlements @@ -0,0 +1,14 @@ + + + + + aps-environment + development + com.apple.developer.icloud-container-identifiers + + com.apple.developer.icloud-services + + CloudKit + + + diff --git a/CxLLM-IOS/CxLLM_IOSApp.swift b/CxLLM-IOS/CxLLM_IOSApp.swift new file mode 100644 index 0000000..8eeac02 --- /dev/null +++ b/CxLLM-IOS/CxLLM_IOSApp.swift @@ -0,0 +1,32 @@ +// +// CxLLM_IOSApp.swift +// CxLLM-IOS +// +// Created by Stephen Carter on 4/22/26. +// + +import SwiftUI +import SwiftData + +@main +struct CxLLM_IOSApp: App { + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + Item.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + return try ModelContainer(for: schema, configurations: [modelConfiguration]) + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(sharedModelContainer) + } +} diff --git a/CxLLM-IOS/Info.plist b/CxLLM-IOS/Info.plist new file mode 100644 index 0000000..ca9a074 --- /dev/null +++ b/CxLLM-IOS/Info.plist @@ -0,0 +1,10 @@ + + + + + UIBackgroundModes + + remote-notification + + + diff --git a/CxLLM-IOS/Item.swift b/CxLLM-IOS/Item.swift new file mode 100644 index 0000000..32775c9 --- /dev/null +++ b/CxLLM-IOS/Item.swift @@ -0,0 +1,18 @@ +// +// Item.swift +// CxLLM-IOS +// +// Created by Stephen Carter on 4/22/26. +// + +import Foundation +import SwiftData + +@Model +final class Item { + var timestamp: Date + + init(timestamp: Date) { + self.timestamp = timestamp + } +} diff --git a/CxLLM-IOSTests/CxLLM_IOSTests.swift b/CxLLM-IOSTests/CxLLM_IOSTests.swift new file mode 100644 index 0000000..880b124 --- /dev/null +++ b/CxLLM-IOSTests/CxLLM_IOSTests.swift @@ -0,0 +1,17 @@ +// +// CxLLM_IOSTests.swift +// CxLLM-IOSTests +// +// Created by Stephen Carter on 4/22/26. +// + +import Testing +@testable import CxLLM_IOS + +struct CxLLM_IOSTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/CxLLM-IOSUITests/CxLLM_IOSUITests.swift b/CxLLM-IOSUITests/CxLLM_IOSUITests.swift new file mode 100644 index 0000000..3dc1cf3 --- /dev/null +++ b/CxLLM-IOSUITests/CxLLM_IOSUITests.swift @@ -0,0 +1,41 @@ +// +// CxLLM_IOSUITests.swift +// CxLLM-IOSUITests +// +// Created by Stephen Carter on 4/22/26. +// + +import XCTest + +final class CxLLM_IOSUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift b/CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift new file mode 100644 index 0000000..9113de4 --- /dev/null +++ b/CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// CxLLM_IOSUITestsLaunchTests.swift +// CxLLM-IOSUITests +// +// Created by Stephen Carter on 4/22/26. +// + +import XCTest + +final class CxLLM_IOSUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/CxLLMStudio/ContentView.swift b/CxLLMStudio/ContentView.swift new file mode 100644 index 0000000..304a5ce --- /dev/null +++ b/CxLLMStudio/ContentView.swift @@ -0,0 +1,67 @@ +// CxLLM Studio — iOS Application +// ContentView.swift — Tab-based navigation for iOS + +import SwiftUI +import CxCode +import CxAWS +import CxGit + +struct ContentView: View { + @Environment(AppState.self) private var appState + @Environment(GatewayService.self) private var gateway + + var body: some View { + @Bindable var state = appState + + TabView(selection: $state.selectedTab) { + Tab("Chat", systemImage: "bubble.left.and.bubble.right.fill", value: AppState.Tab.chat) { + NavigationStack { + ChatView() + .navigationTitle("Chat") + } + } + + Tab("Models", systemImage: "cpu", value: AppState.Tab.models) { + NavigationStack { + ModelsView() + .navigationTitle("CxModels") + } + } + + Tab("AWS", systemImage: "cloud.fill", value: AppState.Tab.aws) { + NavigationStack { + AWSView() + .navigationTitle("AWS DevOps") + } + } + + Tab("Git", systemImage: "arrow.triangle.branch", value: AppState.Tab.git) { + NavigationStack { + GitView() + .navigationTitle("CxGit") + } + } + + Tab("Agent", systemImage: "bolt.fill", value: AppState.Tab.agent) { + NavigationStack { + AgentView() + .navigationTitle("Agent") + } + } + + Tab("MCP", systemImage: "network", value: AppState.Tab.mcp) { + NavigationStack { + MCPView() + .navigationTitle("MCP") + } + } + + Tab("Settings", systemImage: "gear", value: AppState.Tab.settings) { + NavigationStack { + SettingsView() + .navigationTitle("Settings") + } + } + } + } +} diff --git a/CxLLMStudio/CxLLMStudioApp.swift b/CxLLMStudio/CxLLMStudioApp.swift new file mode 100644 index 0000000..ea83ae8 --- /dev/null +++ b/CxLLMStudio/CxLLMStudioApp.swift @@ -0,0 +1,50 @@ +// CxLLM Studio — iOS Application +// CxLLMStudioApp.swift — Main app entry point + +import SwiftUI +import CxCode +import CxAWS +import CxGit + +@main +struct CxLLMStudioApp: App { + @State private var appState = AppState() + @State private var gatewayService: GatewayService + @State private var modelController: CxModelController + @State private var awsService: AWSService + @State private var gitService: GitService + @State private var agentService: AgentService + @State private var mcpService: MCPService + + init() { + let config = CxConfig.fromEnvironment() + let gateway = CxGateway(config: config) + self._gatewayService = State(initialValue: GatewayService(gateway: gateway)) + self._modelController = State(initialValue: CxModelController(gateway: gateway)) + self._awsService = State(initialValue: AWSService(config: CxAWSConfig())) + self._gitService = State(initialValue: GitService()) + self._agentService = State(initialValue: AgentService(gateway: gateway)) + self._mcpService = State(initialValue: MCPService(gateway: gateway)) + } + + var body: some Scene { + WindowGroup { + ContentView() + .environment(appState) + .environment(gatewayService) + .environment(modelController) + .environment(awsService) + .environment(gitService) + .environment(agentService) + .environment(mcpService) + .task { + await gatewayService.checkHealth() + await modelController.loadModels() + await awsService.checkHealth() + await gitService.checkHealth() + await agentService.initialize() + await mcpService.initialize() + } + } + } +} diff --git a/CxLLMStudio/Services/AWSService.swift b/CxLLMStudio/Services/AWSService.swift new file mode 100644 index 0000000..5f0904d --- /dev/null +++ b/CxLLMStudio/Services/AWSService.swift @@ -0,0 +1,95 @@ +// CxLLM Studio — macOS Application +// Services/AWSService.swift — AWS DevOps integration service + +import Foundation +import CxAWS +import CxCode + +@Observable +final class AWSService { + private(set) var isConnected = false + private(set) var healthStatus: AWSHealthStatus? + private(set) var repositories: [[String: Any]] = [] + private(set) var pipelines: [[String: Any]] = [] + private(set) var pullRequests: [[String: Any]] = [] + private(set) var builds: [[String: Any]] = [] + private(set) var error: String? + + let client: CxAWSClient + + init(config: CxAWSConfig) { + self.client = CxAWSClient(config: config) + } + + func checkHealth() async { + let health = await client.healthCheck() + healthStatus = health + isConnected = health.status == "ok" + } + + func loadRepositories() async { + do { + let result = try await client.codecommitListRepositories() + repositories = result["repositories"] as? [[String: Any]] ?? [] + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadPipelines() async { + do { + let result = try await client.codepipelineListPipelines() + pipelines = result["pipelines"] as? [[String: Any]] ?? [] + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadPullRequests(repo: String) async { + do { + let result = try await client.codecommitListPullRequests(repo) + pullRequests = (result["pullRequestIds"] as? [String])?.map { ["id": $0] } ?? [] + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func startPipeline(_ name: String) async throws { + _ = try await client.codepipelineStartExecution(name) + } + + func stopPipeline(_ name: String, executionId: String) async throws { + _ = try await client.codepipelineStopExecution(name, executionId: executionId) + } + + func startBuild(_ project: String) async throws -> [String: Any] { + try await client.codebuildStartBuild(project) + } + + func getBuildStatus(_ ids: [String]) async throws -> [String: Any] { + try await client.codebuildBatchGetBuilds(ids) + } + + func getFile(repo: String, path: String, ref: String = "") async throws -> [String: Any] { + try await client.codecommitGetFile(repo, path: path, ref: ref) + } + + func getPipelineState(_ name: String) async throws -> [String: Any] { + try await client.codepipelineGetPipelineState(name) + } + + func getLogGroups() async throws -> [String: Any] { + try await client.logsDescribeLogGroups(prefix: "/aws/codebuild/") + } + + func getLogStreams(logGroup: String) async throws -> [String: Any] { + try await client.logsDescribeLogStreams(logGroup: logGroup) + } + + func getLogEvents(logGroup: String, logStream: String) async throws -> [String: Any] { + try await client.logsGetLogEvents(logGroup: logGroup, logStream: logStream) + } +} diff --git a/CxLLMStudio/Services/AgentService.swift b/CxLLMStudio/Services/AgentService.swift new file mode 100644 index 0000000..1666159 --- /dev/null +++ b/CxLLMStudio/Services/AgentService.swift @@ -0,0 +1,181 @@ +// CxLLM Studio — iOS Application +// Services/AgentService.swift — Agent operations service + +import Foundation +import CxCode + +@Observable +final class AgentService { + + // MARK: - State + + private(set) var isHealthy = false + private(set) var tools: [AgentToolInfo] = [] + private(set) var isRunning = false + private(set) var isExecutingTool = false + private(set) var error: String? + private(set) var lastHealthCheck: Date? + private(set) var totalExecutions: Int = 0 + private(set) var totalToolCalls: Int = 0 + private(set) var history: [AgentExecution] = [] + + private let gateway: CxGateway + + init(gateway: CxGateway) { + self.gateway = gateway + } + + // MARK: - Health & Discovery + + func checkHealth() async { + do { + let _ = try await gateway.agent.health() + isHealthy = true + error = nil + lastHealthCheck = Date() + } catch { + isHealthy = false + self.error = error.localizedDescription + lastHealthCheck = Date() + } + } + + func loadTools() async { + do { + tools = try await gateway.agent.listTools() + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func initialize() async { + await checkHealth() + if isHealthy { + await loadTools() + } + } + + // MARK: - Autopilot + + func runAutopilot(task: String, maxSteps: Int = 10) async throws -> AgentExecution { + guard isHealthy else { + throw AgentServiceError.notHealthy + } + guard !isRunning else { + throw AgentServiceError.alreadyRunning + } + + isRunning = true + error = nil + let startTime = Date() + + do { + let result = try await gateway.agent.autoPilot(task: task, maxSteps: maxSteps) + let duration = Date().timeIntervalSince(startTime) + let execution = AgentExecution( + task: task, + steps: [], + output: result, + time: Date(), + duration: duration, + success: true + ) + history.insert(execution, at: 0) + totalExecutions += 1 + isRunning = false + return execution + } catch { + let duration = Date().timeIntervalSince(startTime) + let execution = AgentExecution( + task: task, + steps: [], + output: "Error: \(error.localizedDescription)", + time: Date(), + duration: duration, + success: false + ) + history.insert(execution, at: 0) + self.error = error.localizedDescription + isRunning = false + throw error + } + } + + // MARK: - Tool Execution + + func callTool(name: String, arguments: [String: Any]) async throws -> String { + guard !isExecutingTool else { + throw AgentServiceError.alreadyRunning + } + + isExecutingTool = true + error = nil + + do { + let result = try await gateway.agent.callTool(name: name, arguments: arguments) + totalToolCalls += 1 + isExecutingTool = false + return result + } catch { + self.error = error.localizedDescription + isExecutingTool = false + throw error + } + } + + // MARK: - History Management + + func clearHistory() { + history.removeAll() + } + + // MARK: - Computed + + var toolNames: [String] { + tools.map(\.name) + } + + var successRate: Double { + guard !history.isEmpty else { return 0 } + let successes = history.filter(\.success).count + return Double(successes) / Double(history.count) + } + + var averageDuration: TimeInterval { + guard !history.isEmpty else { return 0 } + return history.reduce(0.0) { $0 + $1.duration } / Double(history.count) + } +} + +// MARK: - Supporting Types + +struct AgentStep: Identifiable { + let id = UUID() + let index: Int + let action: String + let result: String + let timestamp: Date +} + +struct AgentExecution: Identifiable { + let id = UUID() + let task: String + let steps: [AgentStep] + let output: String + let time: Date + let duration: TimeInterval + let success: Bool +} + +enum AgentServiceError: LocalizedError { + case notHealthy + case alreadyRunning + + var errorDescription: String? { + switch self { + case .notHealthy: return "Agent service is not healthy. Check connection." + case .alreadyRunning: return "An operation is already in progress." + } + } +} diff --git a/CxLLMStudio/Services/AppState.swift b/CxLLMStudio/Services/AppState.swift new file mode 100644 index 0000000..63f57b2 --- /dev/null +++ b/CxLLMStudio/Services/AppState.swift @@ -0,0 +1,77 @@ +// CxLLM Studio — macOS Application +// Services/AppState.swift — Global application state + +import SwiftUI +import CxCode +import CxAWS + +@Observable +final class AppState { + enum Tab: String, CaseIterable { + case chat, models, aws, git, agent, mcp, settings + } + + var selectedTab: Tab = .chat + var conversations: [Conversation] = [] + var activeConversationId: String? + var selectedModel: CxModelSlot = .spark + + var activeConversation: Conversation? { + get { conversations.first { $0.id == activeConversationId } } + set { + if let newValue, let idx = conversations.firstIndex(where: { $0.id == newValue.id }) { + conversations[idx] = newValue + } + } + } + + func createConversation() { + let conv = Conversation(model: selectedModel) + conversations.insert(conv, at: 0) + activeConversationId = conv.id + } + + func deleteConversation(_ id: String) { + conversations.removeAll { $0.id == id } + if activeConversationId == id { + activeConversationId = conversations.first?.id + } + } +} + +// MARK: - Data Models + +struct Conversation: Identifiable, Codable { + var id: String = UUID().uuidString + var title: String = "New Chat" + var model: CxModelSlot + var messages: [Message] = [] + var createdAt: Date = Date() + var isAgentMode: Bool = false + var sessionId: String? + + var lastMessage: String? { + messages.last?.content + } +} + +struct Message: Identifiable, Codable { + var id: String = UUID().uuidString + var role: Role + var content: String + var timestamp: Date = Date() + var model: String? + var isStreaming: Bool = false + + enum Role: String, Codable { + case system, user, assistant, tool + } + + static func user(_ content: String) -> Message { + Message(role: .user, content: content) + } + + static func assistant(_ content: String, model: String? = nil) -> Message { + Message(role: .assistant, content: content, model: model) + } +} diff --git a/CxLLMStudio/Services/GatewayService.swift b/CxLLMStudio/Services/GatewayService.swift new file mode 100644 index 0000000..0bdb3b8 --- /dev/null +++ b/CxLLMStudio/Services/GatewayService.swift @@ -0,0 +1,48 @@ +// CxLLM Studio — macOS Application +// Services/GatewayService.swift — Gateway connection service + +import Foundation +import CxCode + +@Observable +final class GatewayService { + private(set) var isConnected = false + private(set) var healthStatus: String = "unknown" + private(set) var connectionError: String? + + let gateway: CxGateway + + init(gateway: CxGateway) { + self.gateway = gateway + } + + func checkHealth() async { + do { + let health = try await gateway.health() + healthStatus = health.status + isConnected = health.status == "ok" + connectionError = nil + } catch { + isConnected = false + connectionError = error.localizedDescription + } + } + + func createSession(model: String) async throws -> String { + let session = try await gateway.sessions.create(model: model) + return session.sessionId + } + + func sendMessage(sessionId: String, message: String, model: String) async throws -> ChatResponse { + try await gateway.chat.ask(sessionId: sessionId, message: message, model: model) + } + + func streamMessage(sessionId: String, message: String, model: String) -> AsyncThrowingStream { + gateway.chat.streamAsk(sessionId: sessionId, message: message, model: model) + } + + func configure(baseURL: String, apiKey: String) { + // Recreate gateway with new config would go here + // For now, config is immutable after init + } +} diff --git a/CxLLMStudio/Services/GitService.swift b/CxLLMStudio/Services/GitService.swift new file mode 100644 index 0000000..ed3a13a --- /dev/null +++ b/CxLLMStudio/Services/GitService.swift @@ -0,0 +1,102 @@ +// CxLLM Studio — macOS Application +// Services/GitService.swift — Gitea integration service + +import Foundation +import CxGit +import CxCode + +@Observable +final class GitService { + private(set) var isConnected = false + private(set) var healthStatus: GitHealthStatus? + private(set) var repositories: [GitRepository] = [] + private(set) var pullRequests: [GitPullRequest] = [] + private(set) var issues: [GitIssue] = [] + private(set) var branches: [GitBranch] = [] + private(set) var actionRuns: [GitActionRun] = [] + private(set) var currentUser: GitUser? + private(set) var error: String? + + let client: CxGitClient + + init(config: CxGitConfig = CxGitConfig()) { + self.client = CxGitClient(config: config) + } + + func checkHealth() async { + let health = await client.healthCheck() + healthStatus = health + isConnected = health.isHealthy + } + + func loadRepositories(org: String? = nil) async { + do { + repositories = try await client.listOrgRepos(org) + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadPullRequests(owner: String, repo: String) async { + do { + pullRequests = try await client.listPulls(owner, repo) + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadIssues(owner: String, repo: String) async { + do { + issues = try await client.listIssues(owner, repo) + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadBranches(owner: String, repo: String) async { + do { + branches = try await client.listBranches(owner, repo) + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadActionRuns(owner: String, repo: String) async { + do { + actionRuns = try await client.listActionRuns(owner, repo) + error = nil + } catch { + self.error = error.localizedDescription + } + } + + func loadCurrentUser() async { + do { + currentUser = try await client.authenticatedUser() + } catch { + // Not critical — user may not be authenticated + } + } + + func refreshAll() async { + await checkHealth() + await loadRepositories() + await loadCurrentUser() + } + + func createPullRequest(owner: String, repo: String, title: String, head: String, base: String = "main", body: String = "") async throws -> GitPullRequest { + try await client.createPull(owner, repo, title: title, head: head, base: base, body: body) + } + + func createIssue(owner: String, repo: String, title: String, body: String = "") async throws -> GitIssue { + try await client.createIssue(owner, repo, title: title, body: body) + } + + func createBranch(owner: String, repo: String, name: String, from: String = "main") async throws -> GitBranch { + try await client.createBranch(owner, repo, name: name, from: from) + } +} diff --git a/CxLLMStudio/Services/MCPService.swift b/CxLLMStudio/Services/MCPService.swift new file mode 100644 index 0000000..df7f138 --- /dev/null +++ b/CxLLMStudio/Services/MCPService.swift @@ -0,0 +1,214 @@ +// CxLLM Studio — iOS Application +// Services/MCPService.swift — Model Context Protocol service + +import Foundation +import CxCode + +@Observable +final class MCPService { + + // MARK: - State + + private(set) var isInitialized = false + private(set) var pingOk = false + private(set) var tools: [McpToolDef] = [] + private(set) var resources: [McpResource] = [] + private(set) var error: String? + private(set) var statusMessage: String = "Not initialized" + private(set) var isExecutingTool = false + private(set) var isReadingResource = false + private(set) var history: [MCPHistoryEntry] = [] + private(set) var totalToolCalls: Int = 0 + private(set) var totalResourceReads: Int = 0 + private(set) var lastInitialized: Date? + + private let gateway: CxGateway + + init(gateway: CxGateway) { + self.gateway = gateway + } + + // MARK: - Lifecycle + + func initialize() async { + do { + _ = try await gateway.mcp.initialize() + isInitialized = true + error = nil + lastInitialized = Date() + + if let _ = try? await gateway.mcp.ping() { + pingOk = true + } + + tools = try await gateway.mcp.listTools() + resources = try await gateway.mcp.listResources() + statusMessage = "Ready — \(tools.count) tools, \(resources.count) resources" + } catch { + isInitialized = false + self.error = error.localizedDescription + statusMessage = "Error: \(error.localizedDescription)" + } + } + + func ping() async -> Bool { + do { + let _ = try await gateway.mcp.ping() + pingOk = true + statusMessage = "Pong OK" + error = nil + return true + } catch { + pingOk = false + statusMessage = "Ping failed" + self.error = error.localizedDescription + return false + } + } + + func reload() async { + do { + tools = try await gateway.mcp.listTools() + resources = try await gateway.mcp.listResources() + statusMessage = "Reloaded — \(tools.count) tools, \(resources.count) resources" + error = nil + } catch { + self.error = error.localizedDescription + statusMessage = "Reload failed: \(error.localizedDescription)" + } + } + + // MARK: - Tool Execution + + struct ToolCallResult { + let content: String + let isError: Bool + } + + func callTool(name: String, arguments: [String: Any]) async -> ToolCallResult { + isExecutingTool = true + + do { + let result = try await gateway.mcp.callTool(name: name, arguments: arguments) + let content = result.content?.compactMap(\.text).joined(separator: "\n") ?? "No content" + let isErr = result.isError == true + let displayContent = isErr ? "[ERROR] \(content)" : content + + totalToolCalls += 1 + history.insert( + MCPHistoryEntry(type: .tool, name: name, result: displayContent, time: Date(), success: !isErr), + at: 0 + ) + isExecutingTool = false + error = nil + return ToolCallResult(content: displayContent, isError: isErr) + } catch { + let errMsg = "Error: \(error.localizedDescription)" + history.insert( + MCPHistoryEntry(type: .tool, name: name, result: errMsg, time: Date(), success: false), + at: 0 + ) + self.error = error.localizedDescription + isExecutingTool = false + return ToolCallResult(content: errMsg, isError: true) + } + } + + // MARK: - Resource Reading + + func readResource(uri: String, name: String) async -> String { + isReadingResource = true + + do { + let content = try await gateway.mcp.readResource(uri: uri) + totalResourceReads += 1 + history.insert( + MCPHistoryEntry(type: .resource, name: name, result: String(content.prefix(100)), time: Date(), success: true), + at: 0 + ) + isReadingResource = false + error = nil + return content + } catch { + let errMsg = "Error: \(error.localizedDescription)" + history.insert( + MCPHistoryEntry(type: .resource, name: name, result: errMsg, time: Date(), success: false), + at: 0 + ) + self.error = error.localizedDescription + isReadingResource = false + return errMsg + } + } + + // MARK: - History Management + + func clearHistory() { + history.removeAll() + } + + // MARK: - Computed + + var connectionState: ConnectionState { + if !isInitialized { return .disconnected } + if pingOk { return .live } + return .initialized + } + + var successRate: Double { + guard !history.isEmpty else { return 0 } + let successes = history.filter(\.success).count + return Double(successes) / Double(history.count) + } + + func tool(named name: String) -> McpToolDef? { + tools.first { $0.name == name } + } + + func filteredTools(query: String) -> [McpToolDef] { + guard !query.isEmpty else { return tools } + let q = query.lowercased() + return tools.filter { + $0.name.lowercased().contains(q) || ($0.description ?? "").lowercased().contains(q) + } + } + + func filteredResources(query: String) -> [McpResource] { + guard !query.isEmpty else { return resources } + let q = query.lowercased() + return resources.filter { + $0.name.lowercased().contains(q) || $0.uri.lowercased().contains(q) + } + } +} + +// MARK: - Supporting Types + +enum ConnectionState { + case disconnected, initialized, live + + var label: String { + switch self { + case .disconnected: return "Off" + case .initialized: return "Init" + case .live: return "Live" + } + } + + var isConnected: Bool { + self != .disconnected + } +} + +struct MCPHistoryEntry: Identifiable { + let id = UUID() + let type: EntryType + let name: String + let result: String + let time: Date + let success: Bool + + enum EntryType: String { + case tool, resource + } +} diff --git a/CxLLMStudio/Views/AWS/AWSView.swift b/CxLLMStudio/Views/AWS/AWSView.swift new file mode 100644 index 0000000..0277b49 --- /dev/null +++ b/CxLLMStudio/Views/AWS/AWSView.swift @@ -0,0 +1,132 @@ +// CxLLM Studio — iOS Application +// Views/AWS/AWSView.swift — AWS DevOps dashboard for iOS + +import SwiftUI +import CxAWS + +struct AWSView: View { + @Environment(AWSService.self) private var aws + @State private var selectedSection: AWSSection = .overview + + enum AWSSection: String, CaseIterable, Identifiable { + case overview, repos, pipelines, builds + var id: String { rawValue } + } + + var body: some View { + List { + Picker("Section", selection: $selectedSection) { + ForEach(AWSSection.allCases) { s in + Text(s.rawValue.capitalized).tag(s) + } + } + .pickerStyle(.segmented) + .listRowBackground(Color.clear) + + switch selectedSection { + case .overview: overviewSection + case .repos: reposSection + case .pipelines: pipelinesSection + case .builds: buildsSection + } + } + .task { + await aws.loadRepositories() + await aws.loadPipelines() + } + } + + // MARK: - Overview + + @ViewBuilder + private var overviewSection: some View { + Section("Status") { + LabeledContent("Connection") { + HStack { + Circle() + .fill(aws.isConnected ? .green : .red) + .frame(width: 8, height: 8) + Text(aws.isConnected ? "Connected" : "Offline") + } + } + LabeledContent("Region", value: aws.healthStatus?.region ?? "us-east-2") + LabeledContent("Repositories", value: "\(aws.repositories.count)") + LabeledContent("Pipelines", value: "\(aws.pipelines.count)") + } + + if let error = aws.error { + Section("Error") { + Text(error) + .foregroundStyle(.red) + } + } + } + + // MARK: - Repos + + @ViewBuilder + private var reposSection: some View { + Section("CodeCommit Repositories") { + if aws.repositories.isEmpty { + ContentUnavailableView("No Repositories", systemImage: "chevron.left.forwardslash.chevron.right") + } else { + ForEach(aws.repositories.indices, id: \.self) { idx in + let repo = aws.repositories[idx] + VStack(alignment: .leading) { + Text(repo["repositoryName"] as? String ?? "Unknown") + .font(.headline) + Text(repo["repositoryId"] as? String ?? "") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } + + // MARK: - Pipelines + + @ViewBuilder + private var pipelinesSection: some View { + Section("CodePipeline") { + if aws.pipelines.isEmpty { + ContentUnavailableView("No Pipelines", systemImage: "arrow.triangle.branch") + } else { + ForEach(aws.pipelines.indices, id: \.self) { idx in + let pipeline = aws.pipelines[idx] + HStack { + VStack(alignment: .leading) { + Text(pipeline["name"] as? String ?? "Unknown") + .font(.headline) + Text("Version \(pipeline["version"] as? Int ?? 0)") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Button("Start") { + Task { + try? await aws.startPipeline(pipeline["name"] as? String ?? "") + } + } + .buttonStyle(.bordered) + .tint(.green) + } + } + } + } + } + + // MARK: - Builds + + @ViewBuilder + private var buildsSection: some View { + Section("CodeBuild") { + Button { + Task { _ = try? await aws.startBuild("cxllm-studio-build") } + } label: { + Label("Start Build", systemImage: "hammer.fill") + } + .buttonStyle(.borderedProminent) + } + } +} diff --git a/CxLLMStudio/Views/Agent/AgentView.swift b/CxLLMStudio/Views/Agent/AgentView.swift new file mode 100644 index 0000000..ef5a562 --- /dev/null +++ b/CxLLMStudio/Views/Agent/AgentView.swift @@ -0,0 +1,331 @@ +// CxLLM Studio — iOS Application +// Views/Agent/AgentView.swift — Agent workspace for iOS + +import SwiftUI +import CxCode + +struct AgentView: View { + @Environment(AgentService.self) private var agent + + @State private var taskInput = "" + @State private var maxSteps = 10 + @State private var output = "" + @State private var selectedTool: AgentToolInfo? + @State private var toolArgs = "{}" + @State private var toolResult = "" + @State private var showToolBrowser = false + + private let agentGreen = Color(red: 0.46, green: 0.72, blue: 0.0) + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + statusSection + autopilotSection + quickTasksSection + if !output.isEmpty { outputSection } + toolExecutionSection + if !agent.history.isEmpty { historySection } + } + .padding() + } + .navigationTitle("Agent") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { showToolBrowser = true } label: { + Label("Tools", systemImage: "wrench.and.screwdriver") + } + } + ToolbarItem(placement: .topBarTrailing) { + statusPill + } + } + .sheet(isPresented: $showToolBrowser) { + toolBrowserSheet + } + .task { await agent.initialize() } + } + + // MARK: - Status + + private var statusSection: some View { + HStack(spacing: 12) { + statCard("Tools", "\(agent.tools.count)", "wrench", agentGreen) + statCard("Runs", "\(agent.totalExecutions)", "play.circle", .blue) + statCard("Calls", "\(agent.totalToolCalls)", "arrow.right.circle", .purple) + } + } + + private func statCard(_ label: String, _ value: String, _ icon: String, _ color: Color) -> some View { + VStack(spacing: 4) { + Image(systemName: icon).font(.system(size: 18)).foregroundStyle(color) + Text(value).font(.system(size: 20, weight: .bold, design: .rounded)) + Text(label).font(.caption2).foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(color.opacity(0.06)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + + // MARK: - Autopilot + + private var autopilotSection: some View { + VStack(alignment: .leading, spacing: 8) { + Label("Autopilot", systemImage: "bolt.fill") + .font(.headline).foregroundStyle(agentGreen) + + TextField("Describe a task...", text: $taskInput, axis: .vertical) + .lineLimit(2...5) + .textFieldStyle(.roundedBorder) + + HStack { + HStack(spacing: 4) { + Text("Max steps:").font(.caption).foregroundStyle(.secondary) + TextField("", value: $maxSteps, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 50) + .font(.caption) + } + Spacer() + Button { + runAutopilot() + } label: { + HStack(spacing: 4) { + if agent.isRunning { + ProgressView().controlSize(.small) + } else { + Image(systemName: "play.fill") + } + Text(agent.isRunning ? "Running..." : "Execute") + } + .font(.subheadline.weight(.semibold)) + } + .buttonStyle(.borderedProminent).tint(agentGreen) + .disabled(taskInput.isEmpty || agent.isRunning || !agent.isHealthy) + } + } + } + + // MARK: - Quick Tasks + + private var quickTasksSection: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + quickTaskChip("Analyze project", "magnifyingglass") + quickTaskChip("Fix bugs", "ant") + quickTaskChip("Write tests", "checkmark.circle") + quickTaskChip("Refactor", "arrow.triangle.2.circlepath") + quickTaskChip("Document", "doc.text") + } + } + } + + private func quickTaskChip(_ text: String, _ icon: String) -> some View { + Button { taskInput = text } label: { + Label(text, systemImage: icon).font(.caption) + .padding(.horizontal, 10).padding(.vertical, 6) + .background(Color.primary.opacity(0.05)) + .clipShape(Capsule()) + }.buttonStyle(.plain) + } + + // MARK: - Output + + private var outputSection: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Label("Output", systemImage: "text.alignleft") + .font(.headline) + Spacer() + Button { UIPasteboard.general.string = output } label: { + Image(systemName: "doc.on.doc").font(.caption) + } + } + Text(output) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + + // MARK: - Tool Execution + + private var toolExecutionSection: some View { + VStack(alignment: .leading, spacing: 8) { + Label("Direct Tool Call", systemImage: "terminal") + .font(.headline) + + if let tool = selectedTool { + HStack { + Label(tool.name, systemImage: "wrench").font(.subheadline.weight(.medium)) + Spacer() + Button("Change") { showToolBrowser = true } + .font(.caption).buttonStyle(.bordered).controlSize(.small) + } + + if let desc = tool.description { + Text(desc).font(.caption).foregroundStyle(.secondary) + } + } else { + Button { showToolBrowser = true } label: { + Label("Select a tool", systemImage: "plus.circle") + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + } + .buttonStyle(.bordered) + } + + Text("Arguments (JSON)").font(.caption).foregroundStyle(.secondary) + TextEditor(text: $toolArgs) + .font(.system(.caption, design: .monospaced)) + .scrollContentBackground(.hidden) + .frame(minHeight: 60, maxHeight: 120) + .padding(8) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Button { execTool() } label: { + HStack(spacing: 4) { + if agent.isExecutingTool { + ProgressView().controlSize(.small) + } else { + Image(systemName: "play.fill") + } + Text("Execute") + } + .font(.subheadline.weight(.medium)) + } + .buttonStyle(.borderedProminent).controlSize(.small) + .disabled(selectedTool == nil || agent.isExecutingTool) + + if !toolResult.isEmpty { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Result").font(.caption.weight(.semibold)).foregroundStyle(.secondary) + Spacer() + Button { UIPasteboard.general.string = toolResult } label: { + Image(systemName: "doc.on.doc").font(.caption2) + } + } + Text(toolResult) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + } + + // MARK: - History + + private var historySection: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("History", systemImage: "clock").font(.headline) + Spacer() + Button("Clear") { agent.clearHistory() } + .font(.caption).buttonStyle(.bordered).controlSize(.mini) + } + + ForEach(agent.history) { h in + HStack(spacing: 10) { + Image(systemName: h.success ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundStyle(h.success ? .green : .red) + .font(.body) + VStack(alignment: .leading, spacing: 2) { + Text(h.task).font(.subheadline.weight(.medium)).lineLimit(1) + Text("\(h.steps.count) steps · \(String(format: "%.1fs", h.duration))") + .font(.caption2).foregroundStyle(.secondary) + } + Spacer() + Text(h.time, format: .dateTime.hour().minute()) + .font(.caption2.monospacedDigit()).foregroundStyle(.tertiary) + } + .padding(10) + .background(Color.primary.opacity(0.02)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + + // MARK: - Tool Browser Sheet + + private var toolBrowserSheet: some View { + NavigationStack { + List(agent.tools, id: \.name) { tool in + Button { + selectedTool = tool + showToolBrowser = false + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(tool.name).font(.subheadline.weight(.medium)) + if let desc = tool.description { + Text(desc).font(.caption).foregroundStyle(.secondary).lineLimit(2) + } + } + } + } + .navigationTitle("Agent Tools") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { showToolBrowser = false } + } + } + } + } + + // MARK: - Components + + private var statusPill: some View { + HStack(spacing: 4) { + Circle().fill(agent.isHealthy ? Color.green : Color.red.opacity(0.5)).frame(width: 6, height: 6) + Text(agent.isHealthy ? "Ready" : "Offline").font(.caption2.weight(.semibold)) + } + .padding(.horizontal, 8).padding(.vertical, 4) + .background(agent.isHealthy ? Color.green.opacity(0.08) : Color.red.opacity(0.08)) + .clipShape(Capsule()) + } + + // MARK: - Actions + + private func runAutopilot() { + guard !taskInput.isEmpty else { return } + output = "" + Task { + do { + let execution = try await agent.runAutopilot(task: taskInput, maxSteps: maxSteps) + output = execution.output + } catch { + output = "Error: \(error.localizedDescription)" + } + } + } + + private func execTool() { + guard let tool = selectedTool else { return } + toolResult = "" + Task { + do { + let args = parseJSON(toolArgs) + toolResult = try await agent.callTool(name: tool.name, arguments: args) + } catch { + toolResult = "Error: \(error.localizedDescription)" + } + } + } + + private func parseJSON(_ s: String) -> [String: Any] { + guard let d = s.data(using: .utf8), + let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] + else { return [:] } + return o + } +} diff --git a/CxLLMStudio/Views/Chat/ChatView.swift b/CxLLMStudio/Views/Chat/ChatView.swift new file mode 100644 index 0000000..dcc1d0c --- /dev/null +++ b/CxLLMStudio/Views/Chat/ChatView.swift @@ -0,0 +1,175 @@ +// CxLLM Studio — iOS Application +// Views/Chat/ChatView.swift — iOS chat interface + +import SwiftUI +import CxCode +import CxAWS + +struct ChatView: View { + @Environment(AppState.self) private var appState + @Environment(GatewayService.self) private var gateway + @State private var inputText = "" + @State private var isStreaming = false + @State private var streamingText = "" + + var body: some View { + VStack(spacing: 0) { + if let conv = appState.activeConversation { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(conv.messages) { message in + MessageRow(message: message) + .id(message.id) + } + if isStreaming { + MessageRow(message: Message(role: .assistant, content: streamingText, isStreaming: true)) + .id("streaming") + } + } + .padding() + } + .onChange(of: conv.messages.count) { + if let lastId = conv.messages.last?.id { + proxy.scrollTo(lastId, anchor: .bottom) + } + } + } + + Divider() + + // Input + HStack(spacing: 12) { + TextField("Message...", text: $inputText, axis: .vertical) + .textFieldStyle(.roundedBorder) + .lineLimit(1...5) + + Button { + Task { await sendMessage() } + } label: { + Image(systemName: "arrow.up.circle.fill") + .font(.title2) + } + .disabled(inputText.isEmpty || isStreaming) + } + .padding() + } else { + // Welcome + VStack(spacing: 20) { + Spacer() + Image(systemName: "sparkles") + .font(.system(size: 48)) + .foregroundStyle(.purple) + Text("CxLLM Studio") + .font(.title.bold()) + Text("Tap + to start a conversation") + .foregroundStyle(.secondary) + Button("New Chat") { + appState.createConversation() + } + .buttonStyle(.borderedProminent) + Spacer() + } + } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Menu { + ForEach(CxModelSlot.allCases) { slot in + Button { + appState.selectedModel = slot + appState.createConversation() + } label: { + Label(slot.codename, systemImage: slot.icon) + } + } + } label: { + Image(systemName: "plus") + } + } + + ToolbarItem(placement: .principal) { + HStack(spacing: 6) { + Circle() + .fill(gateway.isConnected ? .green : .red) + .frame(width: 8, height: 8) + Text(appState.selectedModel.codename) + .font(.caption) + } + } + } + } + + private func sendMessage() async { + let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { return } + inputText = "" + + if appState.activeConversationId == nil { + appState.createConversation() + } + + guard var conv = appState.activeConversation else { return } + conv.messages.append(.user(text)) + appState.activeConversation = conv + + if conv.sessionId == nil { + do { + let sessionId = try await gateway.createSession(model: appState.selectedModel.rawValue) + conv.sessionId = sessionId + appState.activeConversation = conv + } catch { return } + } + + guard let sessionId = conv.sessionId else { return } + + isStreaming = true + streamingText = "" + + do { + let response = try await gateway.sendMessage(sessionId: sessionId, message: text, model: appState.selectedModel.rawValue) + conv.messages.append(.assistant(response.reply, model: appState.selectedModel.codename)) + appState.activeConversation = conv + } catch { + conv.messages.append(.assistant("Error: \(error.localizedDescription)")) + appState.activeConversation = conv + } + + isStreaming = false + } +} + +struct MessageRow: View { + let message: Message + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Circle() + .fill(message.role == .user ? .blue : .purple) + .frame(width: 28, height: 28) + .overlay { + Image(systemName: message.role == .user ? "person.fill" : "sparkles") + .font(.caption2) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + if message.isStreaming { + HStack { + Text(message.content) + ProgressView() + } + } else { + Text(LocalizedStringKey(message.content)) + .textSelection(.enabled) + } + if let model = message.model { + Text(model) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + Spacer() + } + } +} diff --git a/CxLLMStudio/Views/Git/GitView.swift b/CxLLMStudio/Views/Git/GitView.swift new file mode 100644 index 0000000..82cf4a6 --- /dev/null +++ b/CxLLMStudio/Views/Git/GitView.swift @@ -0,0 +1,189 @@ +// CxLLM Studio — iOS GitView.swift +// Gitea repository dashboard adapted for iOS + +import SwiftUI +import CxGit + +struct GitView: View { + @Environment(GitService.self) private var git + + enum Section: String, CaseIterable { + case repos = "Repos" + case pulls = "PRs" + case issues = "Issues" + } + + @State private var section: Section = .repos + @State private var selectedRepo: GitRepository? + + var body: some View { + VStack(spacing: 0) { + // Connection bar + HStack(spacing: 8) { + Circle().fill(git.isConnected ? .green : .red).frame(width: 8, height: 8) + Text(git.isConnected ? "Connected" : "Disconnected").font(.caption) + Spacer() + if let user = git.currentUser { + Label(user.login, systemImage: "person.circle").font(.caption).foregroundStyle(.secondary) + } + Button { Task { await git.refreshAll() } } label: { + Image(systemName: "arrow.clockwise").font(.caption) + } + } + .padding(.horizontal).padding(.vertical, 8) + .background(.ultraThinMaterial) + + // Section picker + Picker("Section", selection: $section) { + ForEach(Section.allCases, id: \.self) { s in + Text(s.rawValue).tag(s) + } + } + .pickerStyle(.segmented) + .padding(.horizontal).padding(.vertical, 8) + + // Content + switch section { + case .repos: reposList + case .pulls: pullsList + case .issues: issuesList + } + } + .task { + await git.refreshAll() + } + } + + // MARK: - Repos + + private var reposList: some View { + Group { + if git.repositories.isEmpty { + ContentUnavailableView("No Repositories", systemImage: "folder", + description: Text("Check your Gitea connection")) + } else { + List(git.repositories) { repo in + NavigationLink { + repoDetail(repo) + } label: { + HStack(spacing: 8) { + Image(systemName: repo.private ? "lock.fill" : "globe") + .foregroundStyle(repo.private ? .orange : .green) + .font(.caption) + VStack(alignment: .leading, spacing: 2) { + Text(repo.name).font(.subheadline.weight(.medium)) + if let desc = repo.description, !desc.isEmpty { + Text(desc).font(.caption).foregroundStyle(.secondary).lineLimit(1) + } + } + Spacer() + HStack(spacing: 6) { + Label("\(repo.starsCount)", systemImage: "star").font(.caption2) + Label("\(repo.openIssuesCount)", systemImage: "exclamationmark.circle").font(.caption2) + }.foregroundStyle(.secondary) + } + } + } + } + } + } + + private func repoDetail(_ repo: GitRepository) -> some View { + List { + SwiftUI.Section("Info") { + LabeledContent("Name", value: repo.name) + LabeledContent("Default Branch", value: repo.defaultBranch) + LabeledContent("Stars", value: "\(repo.starsCount)") + LabeledContent("Forks", value: "\(repo.forksCount)") + LabeledContent("Open Issues", value: "\(repo.openIssuesCount)") + if let desc = repo.description { LabeledContent("Description", value: desc) } + } + + SwiftUI.Section { + Button("Load PRs") { + let parts = repo.fullName.components(separatedBy: "/") + if parts.count == 2 { + Task { await git.loadPullRequests(owner: parts[0], repo: parts[1]); section = .pulls } + } + } + Button("Load Issues") { + let parts = repo.fullName.components(separatedBy: "/") + if parts.count == 2 { + Task { await git.loadIssues(owner: parts[0], repo: parts[1]); section = .issues } + } + } + Button("Load Branches") { + let parts = repo.fullName.components(separatedBy: "/") + if parts.count == 2 { + Task { await git.loadBranches(owner: parts[0], repo: parts[1]) } + } + } + } + } + .navigationTitle(repo.name) + } + + // MARK: - Pull Requests + + private var pullsList: some View { + Group { + if git.pullRequests.isEmpty { + ContentUnavailableView("No Pull Requests", systemImage: "arrow.triangle.pull", + description: Text("Select a repo to load PRs")) + } else { + List(git.pullRequests) { pr in + HStack(spacing: 8) { + Image(systemName: pr.state == "open" ? "arrow.triangle.pull" : "checkmark.circle.fill") + .foregroundStyle(pr.state == "open" ? .green : .purple) + VStack(alignment: .leading, spacing: 2) { + Text("#\(pr.number) \(pr.title)").font(.subheadline.weight(.medium)) + HStack(spacing: 6) { + if let user = pr.user { Text(user.login).font(.caption2) } + if let head = pr.head, let base = pr.base { + Text("\(head.ref) → \(base.ref)").font(.caption2).foregroundStyle(.secondary) + } + } + } + Spacer() + Text(pr.state).font(.caption2) + .padding(.horizontal, 6).padding(.vertical, 2) + .background(pr.state == "open" ? Color.green.opacity(0.1) : Color.purple.opacity(0.1)) + .clipShape(Capsule()) + } + } + } + } + } + + // MARK: - Issues + + private var issuesList: some View { + Group { + if git.issues.isEmpty { + ContentUnavailableView("No Issues", systemImage: "exclamationmark.circle", + description: Text("Select a repo to load issues")) + } else { + List(git.issues) { issue in + HStack(spacing: 8) { + Image(systemName: issue.state == "open" ? "circle" : "checkmark.circle.fill") + .foregroundStyle(issue.state == "open" ? .green : .secondary) + VStack(alignment: .leading, spacing: 2) { + Text("#\(issue.number) \(issue.title)").font(.subheadline.weight(.medium)) + HStack(spacing: 4) { + if let user = issue.user { Text(user.login).font(.caption2).foregroundStyle(.secondary) } + if let labels = issue.labels { + ForEach(labels.prefix(3)) { label in + Text(label.name).font(.system(size: 9)) + .padding(.horizontal, 4).padding(.vertical, 1) + .background(Color.secondary.opacity(0.1)).clipShape(Capsule()) + } + } + } + } + Spacer() + } + } + } + } + } +} diff --git a/CxLLMStudio/Views/MCP/MCPView.swift b/CxLLMStudio/Views/MCP/MCPView.swift new file mode 100644 index 0000000..00f90e4 --- /dev/null +++ b/CxLLMStudio/Views/MCP/MCPView.swift @@ -0,0 +1,385 @@ +// CxLLM Studio — iOS Application +// Views/MCP/MCPView.swift — MCP protocol inspector for iOS + +import SwiftUI +import CxCode + +struct MCPView: View { + @Environment(MCPService.self) private var mcp + + enum Tab: String, CaseIterable { + case tools, resources, history + var label: String { rawValue.capitalized } + var icon: String { + switch self { + case .tools: return "wrench.and.screwdriver" + case .resources: return "folder" + case .history: return "clock.arrow.circlepath" + } + } + } + + @State private var tab: Tab = .tools + @State private var selectedToolName: String? + @State private var toolArgs = "{}" + @State private var toolResult = "" + @State private var selectedResource: McpResource? + @State private var resourceContent = "" + @State private var toolSearch = "" + + private let mcpTeal = Color(red: 0.0, green: 0.72, blue: 0.72) + + var body: some View { + VStack(spacing: 0) { + connectionBar + Picker("Tab", selection: $tab) { + ForEach(Tab.allCases, id: \.self) { t in + Label(t.label, systemImage: t.icon).tag(t) + } + } + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.vertical, 8) + + switch tab { + case .tools: toolsTab + case .resources: resourcesTab + case .history: historyTab + } + } + .navigationTitle("MCP") + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button { Task { let _ = await mcp.ping() } } label: { + Image(systemName: "antenna.radiowaves.left.and.right") + } + Button { Task { await mcp.reload() } } label: { + Image(systemName: "arrow.clockwise") + } + } + } + .task { await mcp.initialize() } + } + + // MARK: - Connection Bar + + private var connectionBar: some View { + HStack(spacing: 8) { + Circle() + .fill(mcp.isInitialized ? (mcp.pingOk ? Color.green : .yellow) : Color.red.opacity(0.5)) + .frame(width: 8, height: 8) + Text(mcp.statusMessage) + .font(.caption).foregroundStyle(.secondary).lineLimit(1) + Spacer() + HStack(spacing: 12) { + statLabel("Tools", mcp.tools.count) + statLabel("Resources", mcp.resources.count) + } + } + .padding(.horizontal) + .padding(.vertical, 6) + .background(Color.primary.opacity(0.03)) + } + + private func statLabel(_ label: String, _ count: Int) -> some View { + HStack(spacing: 2) { + Text("\(count)").font(.caption.weight(.bold).monospacedDigit()) + Text(label).font(.caption2).foregroundStyle(.secondary) + } + } + + // MARK: - Tools Tab + + private var toolsTab: some View { + VStack(spacing: 0) { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass").foregroundStyle(.tertiary) + TextField("Search tools...", text: $toolSearch) + .textFieldStyle(.plain) + if !toolSearch.isEmpty { + Button { toolSearch = "" } label: { + Image(systemName: "xmark.circle.fill").foregroundStyle(.tertiary) + } + } + } + .font(.subheadline) + .padding(8) + .background(Color.primary.opacity(0.04)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(.horizontal) + .padding(.bottom, 4) + + List(filteredTools, id: \.name, selection: $selectedToolName) { tool in + NavigationLink(value: tool.name) { + VStack(alignment: .leading, spacing: 2) { + Text(tool.name).font(.subheadline.weight(.medium)) + if let desc = tool.description { + Text(desc).font(.caption).foregroundStyle(.secondary).lineLimit(1) + } + } + } + } + .listStyle(.plain) + .navigationDestination(for: String.self) { name in + if let tool = mcp.tool(named: name) { + toolDetailView(tool) + } + } + } + } + + private var filteredTools: [McpToolDef] { + mcp.filteredTools(query: toolSearch) + } + + private func toolDetailView(_ tool: McpToolDef) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text(tool.name) + .font(.title3.weight(.bold).monospaced()) + + if let desc = tool.description { + Text(desc).font(.subheadline).foregroundStyle(.secondary) + } + + if let schema = tool.inputSchema?.dictValue { + Divider() + Text("Input Schema").font(.caption.weight(.semibold)).foregroundStyle(.tertiary) + schemaView(schema) + } + + Divider() + Text("Arguments (JSON)").font(.caption.weight(.semibold)).foregroundStyle(.tertiary) + TextEditor(text: $toolArgs) + .font(.system(.caption, design: .monospaced)) + .scrollContentBackground(.hidden) + .frame(minHeight: 80) + .padding(8) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Button { callTool(tool.name) } label: { + HStack(spacing: 4) { + if mcp.isExecutingTool { + ProgressView().controlSize(.small) + } else { + Image(systemName: "play.fill") + } + Text("Execute") + } + .font(.subheadline.weight(.medium)) + } + .buttonStyle(.borderedProminent).tint(mcpTeal) + .disabled(mcp.isExecutingTool) + + if !toolResult.isEmpty { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Result").font(.caption.weight(.semibold)).foregroundStyle(.tertiary) + Spacer() + Button { UIPasteboard.general.string = toolResult } label: { + Image(systemName: "doc.on.doc").font(.caption2) + } + } + Text(toolResult) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + .padding() + } + .navigationTitle(tool.name) + .navigationBarTitleDisplayMode(.inline) + } + + private func schemaView(_ dict: [String: Any]) -> some View { + VStack(alignment: .leading, spacing: 4) { + if let props = dict["properties"] as? [String: Any] { + let required = dict["required"] as? [String] ?? [] + ForEach(Array(props.keys.sorted()), id: \.self) { key in + let info = props[key] as? [String: Any] ?? [:] + let typ = info["type"] as? String ?? "any" + let desc = info["description"] as? String + let isReq = required.contains(key) + + VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 4) { + Text(key).font(.caption.weight(.medium).monospaced()) + if isReq { + Text("*").font(.caption.weight(.bold)).foregroundStyle(.red) + } + Spacer() + Text(typ).font(.caption2.monospaced()).foregroundStyle(.tertiary) + } + if let d = desc { + Text(d).font(.caption2).foregroundStyle(.quaternary) + } + } + } + } + } + .padding(8) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + // MARK: - Resources Tab + + private var resourcesTab: some View { + List(mcp.resources, id: \.uri) { resource in + NavigationLink { + resourceDetailView(resource) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(resource.name).font(.subheadline.weight(.medium)) + Text(resource.uri).font(.caption.monospaced()).foregroundStyle(.secondary).lineLimit(1) + if let mime = resource.mimeType { + Text(mime).font(.caption2).foregroundStyle(.tertiary) + .padding(.horizontal, 6).padding(.vertical, 2) + .background(Color.primary.opacity(0.04)) + .clipShape(Capsule()) + } + } + } + } + .listStyle(.plain) + .overlay { + if mcp.resources.isEmpty { + ContentUnavailableView("No Resources", systemImage: "folder", description: Text("Connect to the MCP server to discover resources.")) + } + } + } + + private func resourceDetailView(_ resource: McpResource) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text(resource.name).font(.title3.weight(.bold)) + Text(resource.uri).font(.caption.monospaced()).foregroundStyle(.secondary).textSelection(.enabled) + + if let mime = resource.mimeType { + Text(mime).font(.caption2).foregroundStyle(.tertiary) + .padding(.horizontal, 8).padding(.vertical, 3) + .background(Color.primary.opacity(0.04)) + .clipShape(Capsule()) + } + + if let desc = resource.description { + Text(desc).font(.subheadline).foregroundStyle(.secondary) + } + + Button { readResource(resource) } label: { + HStack(spacing: 4) { + if mcp.isReadingResource { + ProgressView().controlSize(.small) + } else { + Image(systemName: "doc.text.magnifyingglass") + } + Text("Read Content") + } + .font(.subheadline.weight(.medium)) + } + .buttonStyle(.borderedProminent).tint(.blue) + .disabled(mcp.isReadingResource) + + if !resourceContent.isEmpty { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Content").font(.caption.weight(.semibold)).foregroundStyle(.tertiary) + Spacer() + Button { UIPasteboard.general.string = resourceContent } label: { + Image(systemName: "doc.on.doc").font(.caption2) + } + } + Text(resourceContent) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.primary.opacity(0.03)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + .padding() + } + .navigationTitle(resource.name) + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: - History Tab + + private var historyTab: some View { + Group { + if mcp.history.isEmpty { + ContentUnavailableView("No History", + systemImage: "clock.arrow.circlepath", + description: Text("Execute tools or read resources to see history.")) + } else { + List { + Section { + ForEach(mcp.history) { entry in + HStack(spacing: 10) { + Image(systemName: entry.type == .tool ? "wrench" : "doc") + .foregroundStyle(entry.type == .tool ? .green : .blue) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 2) { + Text(entry.name).font(.subheadline.weight(.medium)) + Text(String(entry.result.prefix(80))) + .font(.caption).foregroundStyle(.secondary).lineLimit(1) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text(entry.time, format: .dateTime.hour().minute().second()) + .font(.caption2.monospacedDigit()).foregroundStyle(.tertiary) + Text(entry.success ? "OK" : "ERR") + .font(.caption2.weight(.bold)) + .foregroundStyle(entry.success ? .green : .red) + } + } + } + } header: { + HStack { + Text("\(mcp.history.count) entries") + Spacer() + Button("Clear") { mcp.clearHistory() } + .font(.caption) + } + } + } + .listStyle(.insetGrouped) + } + } + } + + // MARK: - Actions + + private func callTool(_ name: String) { + toolResult = "" + Task { + let result = await mcp.callTool(name: name, arguments: parseJSON(toolArgs)) + toolResult = result.content + } + } + + private func readResource(_ res: McpResource) { + resourceContent = "" + Task { + resourceContent = await mcp.readResource(uri: res.uri, name: res.name) + } + } + + private func parseJSON(_ s: String) -> [String: Any] { + guard let d = s.data(using: .utf8), + let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] + else { return [:] } + return o + } +} diff --git a/CxLLMStudio/Views/Models/ModelsView.swift b/CxLLMStudio/Views/Models/ModelsView.swift new file mode 100644 index 0000000..8c52e24 --- /dev/null +++ b/CxLLMStudio/Views/Models/ModelsView.swift @@ -0,0 +1,179 @@ +// CxLLM Studio — iOS Application +// Views/Models/ModelsView.swift — CxModel browser for iOS + +import SwiftUI +import CxCode +import CxAWS + +struct ModelsView: View { + @Environment(AppState.self) private var appState + @Environment(CxModelController.self) private var modelController + + var body: some View { + List { + ForEach(CxModelSlot.allCases) { slot in + NavigationLink { + ModelDetailView(slot: slot) + } label: { + ModelRow(slot: slot) + } + } + } + } +} + +struct ModelRow: View { + let slot: CxModelSlot + + var body: some View { + HStack(spacing: 12) { + Image(systemName: slot.icon) + .font(.title2) + .foregroundStyle(tierColor(slot.tier)) + .frame(width: 36) + + VStack(alignment: .leading, spacing: 2) { + Text(slot.codename) + .font(.headline) + Text(slot.rawValue) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Text(slot.tier) + .font(.caption2) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(tierColor(slot.tier).opacity(0.15)) + .clipShape(Capsule()) + } + } + + private func tierColor(_ tier: String) -> Color { + switch tier { + case "fast": return .green + case "balanced": return .blue + case "premium": return .purple + case "safety": return .orange + case "ultra": return .red + default: return .gray + } + } +} + +struct ModelDetailView: View { + let slot: CxModelSlot + @Environment(AppState.self) private var appState + @State private var testPrompt = "Hello, introduce yourself in one sentence." + @State private var testResult = "" + @State private var isTesting = false + @Environment(GatewayService.self) private var gateway + + var body: some View { + List { + Section("Model Info") { + LabeledContent("Codename", value: slot.codename) + LabeledContent("Slot", value: slot.rawValue) + LabeledContent("Provider", value: slot.provider) + LabeledContent("Model", value: slot.defaultModel) + LabeledContent("Tier", value: slot.tier) + } + + Section("Capabilities") { + FlowLayout(spacing: 6) { + ForEach(slot.capabilities, id: \.self) { cap in + Text(cap) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(.blue.opacity(0.1)) + .clipShape(Capsule()) + } + } + } + + Section("Recommendation") { + Text(slot.recommendation) + .font(.callout) + .foregroundStyle(.secondary) + } + + Section("Test Inference") { + TextField("Prompt", text: $testPrompt, axis: .vertical) + .lineLimit(2...4) + + Button { + Task { await testInference() } + } label: { + Label(isTesting ? "Testing..." : "Run Test", systemImage: "play.fill") + } + .disabled(isTesting) + + if !testResult.isEmpty { + Text(testResult) + .font(.callout) + .textSelection(.enabled) + } + } + } + .navigationTitle(slot.codename) + } + + private func testInference() async { + isTesting = true + testResult = "" + do { + let sessionId = try await gateway.createSession(model: slot.rawValue) + let response = try await gateway.sendMessage(sessionId: sessionId, message: testPrompt, model: slot.rawValue) + testResult = response.reply + } catch { + testResult = "Error: \(error.localizedDescription)" + } + isTesting = false + } +} + +struct FlowLayout: Layout { + var spacing: CGFloat = 6 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let result = arrangeSubviews(proposal: proposal, subviews: subviews) + return result.size + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let result = arrangeSubviews(proposal: proposal, subviews: subviews) + for (index, position) in result.positions.enumerated() { + subviews[index].place(at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), + proposal: ProposedViewSize(result.sizes[index])) + } + } + + private func arrangeSubviews(proposal: ProposedViewSize, subviews: Subviews) -> (positions: [CGPoint], sizes: [CGSize], size: CGSize) { + let maxWidth = proposal.width ?? .infinity + var positions: [CGPoint] = [] + var sizes: [CGSize] = [] + var x: CGFloat = 0 + var y: CGFloat = 0 + var rowHeight: CGFloat = 0 + var maxX: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x + size.width > maxWidth && x > 0 { + x = 0 + y += rowHeight + spacing + rowHeight = 0 + } + positions.append(CGPoint(x: x, y: y)) + sizes.append(size) + rowHeight = max(rowHeight, size.height) + x += size.width + spacing + maxX = max(maxX, x) + } + + return (positions, sizes, CGSize(width: maxX, height: y + rowHeight)) + } +} diff --git a/CxLLMStudio/Views/Settings/SettingsView.swift b/CxLLMStudio/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..e5abcbb --- /dev/null +++ b/CxLLMStudio/Views/Settings/SettingsView.swift @@ -0,0 +1,90 @@ +// CxLLM Studio — iOS Application +// Views/Settings/SettingsView.swift — iOS app configuration + +import SwiftUI +import CxCode +import CxAWS + +struct SettingsView: View { + @Environment(AppState.self) private var appState + @Environment(GatewayService.self) private var gateway + @Environment(AWSService.self) private var aws + @State private var baseURL = UserDefaults.standard.string(forKey: "cxllm_url") ?? "https://cxllm-studio.com" + @State private var apiKey = UserDefaults.standard.string(forKey: "cxllm_api_key") ?? "" + @State private var awsKey = UserDefaults.standard.string(forKey: "aws_access_key") ?? "" + @State private var awsSecret = UserDefaults.standard.string(forKey: "aws_secret_key") ?? "" + @State private var awsRegion = UserDefaults.standard.string(forKey: "aws_region") ?? "us-east-2" + @State private var healthResult: String? + + var body: some View { + Form { + Section("Gateway") { + TextField("Base URL", text: $baseURL) + .keyboardType(.URL) + .autocapitalization(.none) + SecureField("API Key", text: $apiKey) + } + + Section("AWS") { + TextField("Access Key ID", text: $awsKey) + .autocapitalization(.none) + SecureField("Secret Access Key", text: $awsSecret) + TextField("Region", text: $awsRegion) + .autocapitalization(.none) + } + + Section("Default Model") { + Picker("Model", selection: Bindable(appState).selectedModel) { + ForEach(CxModelSlot.allCases) { slot in + Label(slot.codename, systemImage: slot.icon) + .tag(slot) + } + } + } + + Section("Status") { + LabeledContent("Gateway") { + HStack { + Circle() + .fill(gateway.isConnected ? .green : .red) + .frame(width: 8, height: 8) + Text(gateway.isConnected ? "Connected" : "Offline") + } + } + LabeledContent("AWS") { + HStack { + Circle() + .fill(aws.isConnected ? .green : .red) + .frame(width: 8, height: 8) + Text(aws.isConnected ? "Connected" : "Offline") + } + } + + Button("Check Health") { + Task { + await gateway.checkHealth() + await aws.checkHealth() + healthResult = "Checked" + } + } + } + + Section("About") { + LabeledContent("App", value: "CxLLM Studio 1.0") + LabeledContent("Platform", value: "iOS") + LabeledContent("Models", value: "7 cx-model slots") + } + + Section { + Button("Save Settings") { + UserDefaults.standard.set(baseURL, forKey: "cxllm_url") + UserDefaults.standard.set(apiKey, forKey: "cxllm_api_key") + UserDefaults.standard.set(awsKey, forKey: "aws_access_key") + UserDefaults.standard.set(awsSecret, forKey: "aws_secret_key") + UserDefaults.standard.set(awsRegion, forKey: "aws_region") + } + .buttonStyle(.borderedProminent) + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..114f83a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CxAI-LLM + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8624250 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +# Makefile — common entry points for CxLLM-IOS +SHELL := /bin/bash +.DEFAULT_GOAL := help + +PROJECT := CxLLM-IOS +SCHEME ?= $(PROJECT) +CONFIG ?= Debug +DERIVED ?= build + +.PHONY: help build test lint clean fmt info + +help: ## show this help + @grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-12s %s\n", $$1, $$2}' + +info: ## show project info + @echo "project : $(PROJECT)" + @echo "scheme : $(SCHEME)" + @echo "config : $(CONFIG)" + @echo "derived : $(DERIVED)" + +build: ## xcodebuild build (skip if no .xcodeproj) + @if ls *.xcodeproj 1>/dev/null 2>&1; then \ + xcodebuild -project "$(PROJECT).xcodeproj" -scheme "$(SCHEME)" -configuration "$(CONFIG)" -derivedDataPath "$(DERIVED)" build | xcbeautify || true ; \ + elif [ -f Package.swift ]; then \ + swift build -c $(shell echo "$(CONFIG)" | tr '[:upper:]' '[:lower:]') ; \ + else echo "no buildable target" ; fi + +test: ## xcodebuild test + @if ls *.xcodeproj 1>/dev/null 2>&1; then \ + xcodebuild -project "$(PROJECT).xcodeproj" -scheme "$(SCHEME)" -configuration "$(CONFIG)" -derivedDataPath "$(DERIVED)" test | xcbeautify || true ; \ + elif [ -f Package.swift ]; then \ + swift test ; \ + else echo "no testable target" ; fi + +lint: ## swiftlint + swiftformat (no-op if missing) + @command -v swiftlint >/dev/null && swiftlint --quiet || echo "swiftlint not installed" + @command -v swiftformat >/dev/null && swiftformat --lint . || echo "swiftformat not installed" + +fmt: ## swiftformat in-place + @command -v swiftformat >/dev/null && swiftformat . || echo "swiftformat not installed" + +clean: ## remove build artifacts + rm -rf "$(DERIVED)" .build DerivedData diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..0c3779f --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.9 +// CxLLM Studio — iOS Application + +import PackageDescription + +let package = Package( + name: "CxLLMStudio-iOS", + platforms: [.iOS(.v17)], + dependencies: [ + .package(url: "https://git.cxllm-studio.com/CxAI-LLM/CxLLM-SDK.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "CxLLMStudio", + dependencies: [ + .product(name: "CxCode", package: "CxLLM-SDK"), + .product(name: "CxAWS", package: "CxLLM-SDK"), + .product(name: "CxGit", package: "CxLLM-SDK"), + ], + path: "CxLLMStudio" + ), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ee726d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# CxLLM-IOS + +> CxLLM iOS application + +SwiftUI/SwiftData iOS host application embedding CxLLM.framework with on-device inference, App Group sharing, and background tasks. + +[![ci](https://git.cxllm-studio.com/CxAI-LLM/CxLLM-iOS/actions/workflows/ci.yml/badge.svg)](https://git.cxllm-studio.com/CxAI-LLM/CxLLM-iOS/actions) +[![license](https://img.shields.io/badge/license-MIT-7C3AED)](LICENSE) +[![category](https://img.shields.io/badge/category-app-ios-1F6FEB)](#) + +## Overview + +`CxLLM-IOS` is part of the **CxLLM** product family — a layered runtime that +spans Apple kernel extensions, user-space frameworks, host applications, and +spatial / web surfaces. This module focuses on **app-ios** concerns and is +intentionally narrow so that the CxLLM monorepo can compose targets without +pulling in unrelated dependencies. + +## Repository layout + +- `CxLLM-IOS/` — primary source folder (matches the Xcode group / SwiftPM root). +- `CxLLM-IOS.xcodeproj` — Xcode project (where applicable). +- `Makefile` — common entry points (`build`, `test`, `lint`, `clean`). +- `.github/workflows/ci.yml` — CI pipeline (Xcode build + lint). +- `docs/` — architecture notes and ADRs. + +## Quick start + +```bash +# Build (defaults to Debug for the host platform): +make build + +# Run the full test suite (unit + UI where applicable): +make test + +# Static analysis + format check: +make lint +``` + +## Versioning + +This module follows [Semantic Versioning 2.0](https://semver.org/) and is +released in lock-step with the umbrella **CxLLM** product line. See +[`CHANGELOG.md`](CHANGELOG.md) for the user-facing change log. + +## Security + +Please report security issues per [`SECURITY.md`](SECURITY.md). Do **not** +open public issues for vulnerabilities. + +## License + +Released under the [MIT License](LICENSE) © 2026 CxAI-LLM. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1081a36 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security policy for CxLLM-IOS + +## Reporting a vulnerability + +Please email **security@cxllm-studio.com** with: + +- A description of the vulnerability and its impact. +- Steps to reproduce, ideally with a minimal proof-of-concept. +- The affected version(s) / commit SHAs. + +We aim to acknowledge within **2 business days** and to publish a fix or +mitigation within **30 days** for high-severity issues. + +Do **not** open a public Gitea / GitHub issue for vulnerabilities. + +## Supported versions + +Only the `main` branch and the most recent tagged release receive security +updates. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..17210f7 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,24 @@ +# Architecture — CxLLM-IOS + +> CxLLM iOS application + +## Goals + +- Provide a focused, well-tested app-ios surface for the CxLLM family. +- Stay deployable on its own (no monorepo coupling). +- Keep the public ABI small and explicitly versioned. + +## Boundaries + +```text + +----------------------+ + client --> | CxLLM-IOS | + +----------+-----------+ + | + v + CxLLM runtime / kernel +``` + +## Decisions + +- See `docs/adr/` for Architecture Decision Records. diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md new file mode 100644 index 0000000..4fc033a --- /dev/null +++ b/docs/adr/0001-record-architecture-decisions.md @@ -0,0 +1,13 @@ +# 1. Record architecture decisions + +## Status +Accepted + +## Context +We need a lightweight, append-only log of architectural choices. + +## Decision +Use Markdown ADRs in `docs/adr/`, numbered sequentially. + +## Consequences +Future maintainers can read the chain of decisions without spelunking PR history.