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