chore: initial commit (Phase 3 scaffold)
This commit is contained in:
commit
6af426e57c
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@ -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
|
||||
36
.gitea/workflows/ci.yml
Normal file
36
.gitea/workflows/ci.yml
Normal file
@ -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
|
||||
27
.github/workflows/ci.yml
vendored
Normal file
27
.github/workflows/ci.yml
vendored
Normal file
@ -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
|
||||
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@ -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
|
||||
16
CHANGELOG.md
Normal file
16
CHANGELOG.md
Normal file
@ -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.
|
||||
6
CODEOWNERS
Normal file
6
CODEOWNERS
Normal file
@ -0,0 +1,6 @@
|
||||
# Default owners for everything in this repo.
|
||||
* @CxAI-LLM/maintainers
|
||||
|
||||
# Build & CI
|
||||
/.github/ @CxAI-LLM/devops
|
||||
/Makefile @CxAI-LLM/devops
|
||||
23
CONTRIBUTING.md
Normal file
23
CONTRIBUTING.md
Normal file
@ -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).
|
||||
600
CxLLM-IOS.xcodeproj/project.pbxproj
Normal file
600
CxLLM-IOS.xcodeproj/project.pbxproj
Normal file
@ -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 = "<group>";
|
||||
};
|
||||
20F2AF352F99700100E7D2D9 /* CxLLM-IOSTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "CxLLM-IOSTests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
20F2AF3F2F99700100E7D2D9 /* CxLLM-IOSUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "CxLLM-IOSUITests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
20F2AF222F99700000E7D2D9 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
20F2AF212F99700000E7D2D9 /* CxLLM-IOS.app */,
|
||||
20F2AF322F99700100E7D2D9 /* CxLLM-IOSTests.xctest */,
|
||||
20F2AF3C2F99700100E7D2D9 /* CxLLM-IOSUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
7
CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
CxLLM-IOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
11
CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
11
CxLLM-IOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
35
CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
35
CxLLM-IOS/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
6
CxLLM-IOS/Assets.xcassets/Contents.json
Normal file
6
CxLLM-IOS/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
61
CxLLM-IOS/ContentView.swift
Normal file
61
CxLLM-IOS/ContentView.swift
Normal file
@ -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)
|
||||
}
|
||||
14
CxLLM-IOS/CxLLM_IOS.entitlements
Normal file
14
CxLLM-IOS/CxLLM_IOS.entitlements
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
32
CxLLM-IOS/CxLLM_IOSApp.swift
Normal file
32
CxLLM-IOS/CxLLM_IOSApp.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
10
CxLLM-IOS/Info.plist
Normal file
10
CxLLM-IOS/Info.plist
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
18
CxLLM-IOS/Item.swift
Normal file
18
CxLLM-IOS/Item.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
17
CxLLM-IOSTests/CxLLM_IOSTests.swift
Normal file
17
CxLLM-IOSTests/CxLLM_IOSTests.swift
Normal file
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
41
CxLLM-IOSUITests/CxLLM_IOSUITests.swift
Normal file
41
CxLLM-IOSUITests/CxLLM_IOSUITests.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
33
CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift
Normal file
33
CxLLM-IOSUITests/CxLLM_IOSUITestsLaunchTests.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
67
CxLLMStudio/ContentView.swift
Normal file
67
CxLLMStudio/ContentView.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
CxLLMStudio/CxLLMStudioApp.swift
Normal file
50
CxLLMStudio/CxLLMStudioApp.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
CxLLMStudio/Services/AWSService.swift
Normal file
95
CxLLMStudio/Services/AWSService.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
181
CxLLMStudio/Services/AgentService.swift
Normal file
181
CxLLMStudio/Services/AgentService.swift
Normal file
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
77
CxLLMStudio/Services/AppState.swift
Normal file
77
CxLLMStudio/Services/AppState.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
48
CxLLMStudio/Services/GatewayService.swift
Normal file
48
CxLLMStudio/Services/GatewayService.swift
Normal file
@ -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<String, Error> {
|
||||
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
|
||||
}
|
||||
}
|
||||
102
CxLLMStudio/Services/GitService.swift
Normal file
102
CxLLMStudio/Services/GitService.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
214
CxLLMStudio/Services/MCPService.swift
Normal file
214
CxLLMStudio/Services/MCPService.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
132
CxLLMStudio/Views/AWS/AWSView.swift
Normal file
132
CxLLMStudio/Views/AWS/AWSView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
331
CxLLMStudio/Views/Agent/AgentView.swift
Normal file
331
CxLLMStudio/Views/Agent/AgentView.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
175
CxLLMStudio/Views/Chat/ChatView.swift
Normal file
175
CxLLMStudio/Views/Chat/ChatView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
189
CxLLMStudio/Views/Git/GitView.swift
Normal file
189
CxLLMStudio/Views/Git/GitView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
385
CxLLMStudio/Views/MCP/MCPView.swift
Normal file
385
CxLLMStudio/Views/MCP/MCPView.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
179
CxLLMStudio/Views/Models/ModelsView.swift
Normal file
179
CxLLMStudio/Views/Models/ModelsView.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
90
CxLLMStudio/Views/Settings/SettingsView.swift
Normal file
90
CxLLMStudio/Views/Settings/SettingsView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
||||
43
Makefile
Normal file
43
Makefile
Normal file
@ -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
|
||||
23
Package.swift
Normal file
23
Package.swift
Normal file
@ -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"
|
||||
),
|
||||
]
|
||||
)
|
||||
53
README.md
Normal file
53
README.md
Normal file
@ -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.
|
||||
|
||||
[](https://git.cxllm-studio.com/CxAI-LLM/CxLLM-iOS/actions)
|
||||
[](LICENSE)
|
||||
[](#)
|
||||
|
||||
## 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.
|
||||
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
@ -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.
|
||||
24
docs/ARCHITECTURE.md
Normal file
24
docs/ARCHITECTURE.md
Normal file
@ -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.
|
||||
13
docs/adr/0001-record-architecture-decisions.md
Normal file
13
docs/adr/0001-record-architecture-decisions.md
Normal file
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user