diff --git a/apps/ios-github-mobile/Package.swift b/apps/ios-github-mobile/Package.swift
new file mode 100644
index 000000000..5ae9f673a
--- /dev/null
+++ b/apps/ios-github-mobile/Package.swift
@@ -0,0 +1,22 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "GitHubMobile",
+ platforms: [
+ .iOS(.v17),
+ .macOS(.v14)
+ ],
+ products: [
+ .library(
+ name: "GitHubMobile",
+ targets: ["GitHubMobile"]
+ )
+ ],
+ targets: [
+ .target(
+ name: "GitHubMobile",
+ path: "Sources"
+ )
+ ]
+)
diff --git a/apps/ios-github-mobile/README.md b/apps/ios-github-mobile/README.md
new file mode 100644
index 000000000..e98229a69
--- /dev/null
+++ b/apps/ios-github-mobile/README.md
@@ -0,0 +1,96 @@
+# GitHub Mobile - SwiftUI App
+
+A beautiful SwiftUI mobile app inspired by the Claude Code mobile experience. This app provides a clean interface for managing GitHub repositories and sending code requests through an AI assistant.
+
+## Features
+
+- **Session Management**: View and manage coding sessions organized by repository
+- **Chat Interface**: Send messages and view AI responses with tool execution details
+- **Repository Selection**: Browse and select from your GitHub repositories
+- **Model Selection**: Choose between different AI models (Sonnet, Opus, Haiku)
+- **Dark Theme**: Beautiful dark theme matching the Claude Code aesthetic
+- **PR Creation**: Simulate creating pull requests from the app
+
+## Screenshots
+
+The app is inspired by the Claude Code mobile app UI:
+- Session list with repository information
+- Chat view with collapsible tool executions (Write, Bash, Read, etc.)
+- Bottom input with model/repo selection chips
+
+## Requirements
+
+- iOS 17.0+
+- Xcode 15.0+
+- Swift 5.9+
+
+## Project Structure
+
+```
+Sources/
+├── GitHubMobileApp.swift # App entry point
+├── ContentView.swift # Main navigation container
+├── Models/
+│ ├── Models.swift # Data models (Session, Message, Repository, etc.)
+│ └── AppState.swift # Observable app state
+├── Views/
+│ ├── SessionListView.swift # Session list screen
+│ └── ChatView.swift # Chat/conversation screen
+├── Components/
+│ ├── MessageInputView.swift # Message input component
+│ ├── PickerSheets.swift # Model and repo picker sheets
+│ └── StatusIndicator.swift # Status indicators and badges
+├── Theme/
+│ └── AppTheme.swift # Colors, spacing, and styling
+└── Assets.xcassets/ # App icons and colors
+```
+
+## Architecture
+
+The app uses modern SwiftUI patterns:
+- **@Observable** macro for state management (iOS 17+)
+- **NavigationStack** with typed navigation paths
+- **Environment** for dependency injection
+- **View modifiers** for reusable styling
+
+## Building
+
+### Using Xcode
+
+1. Open `Package.swift` in Xcode
+2. Select your target device
+3. Build and run (Cmd+R)
+
+### Using Swift Package Manager
+
+```bash
+swift build
+```
+
+## Mock Data
+
+The app includes mock data for demonstration:
+- Sample repositories (clawdbot, systemss, Awesome-Prompts, etc.)
+- Sample sessions with various titles
+- Mock tool executions (Write, Bash commands)
+
+## Customization
+
+### Colors
+
+Edit `Theme/AppTheme.swift` to customize:
+- Background colors
+- Accent colors (warm orange by default)
+- Text colors
+- Tool execution colors
+
+### Mock Data
+
+Edit `Models/Models.swift` to customize:
+- `Repository.mockList` - Available repositories
+- `Session.mockList` - Session list items
+- `Session.mockActiveSession()` - Active session with tool calls
+
+## License
+
+Open source - feel free to use and modify.
diff --git a/apps/ios-github-mobile/Sources/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios-github-mobile/Sources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..5ee974dd8
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.350",
+ "green" : "0.550",
+ "red" : "0.850"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/ios-github-mobile/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..9f750565c
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "idiom" : "universal",
+ "platform" : "macos",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Assets.xcassets/Contents.json b/apps/ios-github-mobile/Sources/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json b/apps/ios-github-mobile/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 000000000..cf1991f02
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.080",
+ "green" : "0.080",
+ "red" : "0.080"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Components/MessageInputView.swift b/apps/ios-github-mobile/Sources/Components/MessageInputView.swift
new file mode 100644
index 000000000..d5c30eb6c
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Components/MessageInputView.swift
@@ -0,0 +1,70 @@
+import SwiftUI
+
+struct MessageInputView: View {
+ @Binding var text: String
+ let placeholder: String
+ let onSend: () -> Void
+
+ @FocusState private var isFocused: Bool
+
+ var body: some View {
+ HStack(alignment: .bottom, spacing: AppTheme.spacingSM) {
+ // Add attachment button
+ Button {
+ // Add attachment action
+ } label: {
+ Image(systemName: "plus")
+ .font(.title3)
+ .foregroundStyle(AppTheme.secondaryText)
+ .frame(width: 36, height: 36)
+ }
+
+ // Text input
+ ZStack(alignment: .leading) {
+ if text.isEmpty {
+ Text(placeholder)
+ .foregroundStyle(AppTheme.tertiaryText)
+ .padding(.horizontal, AppTheme.spacingMD)
+ .padding(.vertical, AppTheme.spacingMD)
+ }
+
+ TextEditor(text: $text)
+ .scrollContentBackground(.hidden)
+ .padding(.horizontal, AppTheme.spacingSM)
+ .padding(.vertical, AppTheme.spacingSM)
+ .frame(minHeight: 40, maxHeight: 120)
+ .focused($isFocused)
+ }
+ .background(AppTheme.cardBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusLG))
+
+ // Send button
+ Button(action: onSend) {
+ Image(systemName: "arrow.up")
+ .font(.body)
+ .fontWeight(.semibold)
+ .foregroundStyle(text.isEmpty ? AppTheme.tertiaryText : AppTheme.buttonPrimaryText)
+ .frame(width: 36, height: 36)
+ .background(text.isEmpty ? AppTheme.chipBackground : AppTheme.accent)
+ .clipShape(Circle())
+ }
+ .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
+ }
+ .padding(.horizontal, AppTheme.spacingMD)
+ }
+}
+
+#Preview {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+ VStack {
+ Spacer()
+ MessageInputView(
+ text: .constant(""),
+ placeholder: "Ask Claude to help with code...",
+ onSend: {}
+ )
+ }
+ }
+ .preferredColorScheme(.dark)
+}
diff --git a/apps/ios-github-mobile/Sources/Components/PickerSheets.swift b/apps/ios-github-mobile/Sources/Components/PickerSheets.swift
new file mode 100644
index 000000000..147b8b93c
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Components/PickerSheets.swift
@@ -0,0 +1,155 @@
+import SwiftUI
+
+// MARK: - Model Picker Sheet
+
+struct ModelPickerSheet: View {
+ @Environment(\.dismiss) private var dismiss
+ @Binding var selectedModel: AIModel
+
+ var body: some View {
+ NavigationStack {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ List {
+ ForEach(AIModel.all) { model in
+ Button {
+ selectedModel = model
+ dismiss()
+ } label: {
+ HStack {
+ VStack(alignment: .leading, spacing: AppTheme.spacingXS) {
+ Text(model.displayName)
+ .font(.body)
+ .foregroundStyle(AppTheme.primaryText)
+
+ Text(modelDescription(for: model))
+ .font(.caption)
+ .foregroundStyle(AppTheme.secondaryText)
+ }
+
+ Spacer()
+
+ if model.id == selectedModel.id {
+ Image(systemName: "checkmark")
+ .foregroundStyle(AppTheme.accent)
+ }
+ }
+ .padding(.vertical, AppTheme.spacingXS)
+ }
+ }
+ .listRowBackground(AppTheme.cardBackground)
+ }
+ .scrollContentBackground(.hidden)
+ }
+ .navigationTitle("Select Model")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Done") {
+ dismiss()
+ }
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ }
+ }
+
+ private func modelDescription(for model: AIModel) -> String {
+ switch model.name {
+ case "sonnet":
+ return "Balanced performance and speed"
+ case "opus":
+ return "Most capable, complex tasks"
+ case "haiku":
+ return "Fast and efficient"
+ default:
+ return ""
+ }
+ }
+}
+
+// MARK: - Repository Picker Sheet
+
+struct RepositoryPickerSheet: View {
+ @Environment(\.dismiss) private var dismiss
+ let repositories: [Repository]
+ @Binding var selectedRepository: Repository?
+ @State private var searchText = ""
+
+ private var filteredRepositories: [Repository] {
+ if searchText.isEmpty {
+ return repositories
+ }
+ return repositories.filter {
+ $0.name.localizedCaseInsensitiveContains(searchText) ||
+ $0.owner.localizedCaseInsensitiveContains(searchText)
+ }
+ }
+
+ var body: some View {
+ NavigationStack {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ List {
+ ForEach(filteredRepositories) { repo in
+ Button {
+ selectedRepository = repo
+ dismiss()
+ } label: {
+ HStack {
+ Image(systemName: "folder")
+ .foregroundStyle(AppTheme.secondaryText)
+
+ VStack(alignment: .leading, spacing: AppTheme.spacingXS) {
+ Text(repo.name)
+ .font(.body)
+ .foregroundStyle(AppTheme.primaryText)
+
+ Text(repo.owner)
+ .font(.caption)
+ .foregroundStyle(AppTheme.secondaryText)
+ }
+
+ Spacer()
+
+ if repo.id == selectedRepository?.id {
+ Image(systemName: "checkmark")
+ .foregroundStyle(AppTheme.accent)
+ }
+ }
+ .padding(.vertical, AppTheme.spacingXS)
+ }
+ }
+ .listRowBackground(AppTheme.cardBackground)
+ }
+ .scrollContentBackground(.hidden)
+ .searchable(text: $searchText, prompt: "Search repositories")
+ }
+ .navigationTitle("Select Repository")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Done") {
+ dismiss()
+ }
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ }
+ }
+}
+
+#Preview("Model Picker") {
+ ModelPickerSheet(selectedModel: .constant(.sonnet))
+ .preferredColorScheme(.dark)
+}
+
+#Preview("Repository Picker") {
+ RepositoryPickerSheet(
+ repositories: Repository.mockList,
+ selectedRepository: .constant(nil)
+ )
+ .preferredColorScheme(.dark)
+}
diff --git a/apps/ios-github-mobile/Sources/Components/StatusIndicator.swift b/apps/ios-github-mobile/Sources/Components/StatusIndicator.swift
new file mode 100644
index 000000000..6d10dffac
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Components/StatusIndicator.swift
@@ -0,0 +1,108 @@
+import SwiftUI
+
+struct StatusIndicator: View {
+ let status: SessionStatus
+
+ var body: some View {
+ HStack(spacing: AppTheme.spacingXS) {
+ Circle()
+ .fill(statusColor)
+ .frame(width: 8, height: 8)
+
+ Text(status.rawValue)
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+ }
+ }
+
+ private var statusColor: Color {
+ switch status {
+ case .idle:
+ return AppTheme.secondaryText
+ case .running:
+ return AppTheme.accent
+ case .completed:
+ return AppTheme.success
+ case .error:
+ return AppTheme.error
+ }
+ }
+}
+
+// MARK: - Loading Indicator
+
+struct LoadingIndicator: View {
+ @State private var isAnimating = false
+
+ var body: some View {
+ HStack(spacing: AppTheme.spacingXS) {
+ ForEach(0..<3) { index in
+ Circle()
+ .fill(AppTheme.accent)
+ .frame(width: 6, height: 6)
+ .scaleEffect(isAnimating ? 1.0 : 0.5)
+ .animation(
+ .easeInOut(duration: 0.6)
+ .repeatForever()
+ .delay(Double(index) * 0.2),
+ value: isAnimating
+ )
+ }
+ }
+ .onAppear {
+ isAnimating = true
+ }
+ }
+}
+
+// MARK: - Tool Status Badge
+
+struct ToolStatusBadge: View {
+ let status: ToolCallStatus
+
+ var body: some View {
+ HStack(spacing: AppTheme.spacingXS) {
+ switch status {
+ case .pending:
+ Image(systemName: "clock")
+ .foregroundStyle(AppTheme.secondaryText)
+ case .running:
+ ProgressView()
+ .scaleEffect(0.7)
+ .tint(AppTheme.accent)
+ case .completed:
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundStyle(AppTheme.success)
+ case .failed:
+ Image(systemName: "xmark.circle.fill")
+ .foregroundStyle(AppTheme.error)
+ }
+ }
+ .font(.caption)
+ }
+}
+
+#Preview {
+ VStack(spacing: 20) {
+ StatusIndicator(status: .idle)
+ StatusIndicator(status: .running)
+ StatusIndicator(status: .completed)
+ StatusIndicator(status: .error)
+
+ Divider()
+
+ LoadingIndicator()
+
+ Divider()
+
+ HStack(spacing: 20) {
+ ToolStatusBadge(status: .pending)
+ ToolStatusBadge(status: .running)
+ ToolStatusBadge(status: .completed)
+ ToolStatusBadge(status: .failed)
+ }
+ }
+ .padding()
+ .background(AppTheme.background)
+ .preferredColorScheme(.dark)
+}
diff --git a/apps/ios-github-mobile/Sources/ContentView.swift b/apps/ios-github-mobile/Sources/ContentView.swift
new file mode 100644
index 000000000..17bc051df
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/ContentView.swift
@@ -0,0 +1,20 @@
+import SwiftUI
+
+struct ContentView: View {
+ @Environment(AppState.self) private var appState
+
+ var body: some View {
+ NavigationStack(path: Bindable(appState).navigationPath) {
+ SessionListView()
+ .navigationDestination(for: Session.self) { session in
+ ChatView(session: session)
+ }
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+ .environment(AppState())
+ .preferredColorScheme(.dark)
+}
diff --git a/apps/ios-github-mobile/Sources/GitHubMobileApp.swift b/apps/ios-github-mobile/Sources/GitHubMobileApp.swift
new file mode 100644
index 000000000..27e5b3fb9
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/GitHubMobileApp.swift
@@ -0,0 +1,14 @@
+import SwiftUI
+
+@main
+struct GitHubMobileApp: App {
+ @State private var appState = AppState()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environment(appState)
+ .preferredColorScheme(.dark)
+ }
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Info.plist b/apps/ios-github-mobile/Sources/Info.plist
new file mode 100644
index 000000000..7c70aae45
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Info.plist
@@ -0,0 +1,55 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Code
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchScreen
+
+ UIColorName
+ LaunchBackground
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIUserInterfaceStyle
+ Dark
+
+
diff --git a/apps/ios-github-mobile/Sources/Models/AppState.swift b/apps/ios-github-mobile/Sources/Models/AppState.swift
new file mode 100644
index 000000000..6a5bb19ca
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Models/AppState.swift
@@ -0,0 +1,92 @@
+import Foundation
+import Observation
+import SwiftUI
+
+@Observable
+@MainActor
+final class AppState {
+ var sessions: [Session] = Session.mockList
+ var repositories: [Repository] = Repository.mockList
+ var selectedModel: AIModel = .sonnet
+ var currentSession: Session?
+ var navigationPath = NavigationPath()
+
+ // Current input state
+ var messageInput: String = ""
+ var selectedRepository: Repository?
+ var selectedBranch: String = "main"
+
+ init() {
+ self.selectedRepository = repositories.first
+ }
+
+ func createNewSession(title: String, repository: Repository) -> Session {
+ let session = Session(
+ id: UUID().uuidString,
+ title: title,
+ repository: repository,
+ createdAt: Date(),
+ status: .idle,
+ messages: []
+ )
+ sessions.insert(session, at: 0)
+ return session
+ }
+
+ func sendMessage(_ content: String, to session: Session) {
+ guard let index = sessions.firstIndex(where: { $0.id == session.id }) else { return }
+
+ let message = Message(
+ id: UUID().uuidString,
+ role: .user,
+ content: content,
+ timestamp: Date(),
+ toolCalls: []
+ )
+
+ sessions[index].messages.append(message)
+ sessions[index].status = .running
+
+ // Simulate assistant response after a delay
+ simulateResponse(for: sessions[index])
+ }
+
+ private func simulateResponse(for session: Session) {
+ guard let index = sessions.firstIndex(where: { $0.id == session.id }) else { return }
+
+ // Create a simulated assistant response
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
+ guard let self else { return }
+
+ let assistantMessage = Message(
+ id: UUID().uuidString,
+ role: .assistant,
+ content: "Anladım! Şimdi bu değişiklikleri yapmaya başlıyorum...",
+ timestamp: Date(),
+ toolCalls: [
+ ToolCall(
+ id: UUID().uuidString,
+ type: .read,
+ name: "Read",
+ input: "/home/user/\(session.repository.name)/README.md",
+ output: "# \(session.repository.name)\n\nProject description...",
+ status: .completed
+ )
+ ]
+ )
+
+ self.sessions[index].messages.append(assistantMessage)
+ self.sessions[index].status = .idle
+ }
+ }
+
+ func openSession(_ session: Session) {
+ currentSession = session
+ navigationPath.append(session)
+ }
+
+ func selectRepository(_ repository: Repository) {
+ selectedRepository = repository
+ selectedBranch = repository.defaultBranch
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Models/Models.swift b/apps/ios-github-mobile/Sources/Models/Models.swift
new file mode 100644
index 000000000..6b02adbbb
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Models/Models.swift
@@ -0,0 +1,277 @@
+import Foundation
+
+// MARK: - Repository
+
+struct Repository: Identifiable, Hashable, Codable {
+ let id: String
+ let name: String
+ let owner: String
+ let defaultBranch: String
+
+ var fullName: String {
+ "\(owner)/\(name)"
+ }
+
+ static let mock = Repository(
+ id: "1",
+ name: "clawdbot",
+ owner: "semihpolat",
+ defaultBranch: "main"
+ )
+
+ static let mockList: [Repository] = [
+ Repository(id: "1", name: "clawdbot", owner: "semihpolat", defaultBranch: "main"),
+ Repository(id: "2", name: "systemss", owner: "semihpolat", defaultBranch: "main"),
+ Repository(id: "3", name: "Awesome-Prompts", owner: "semihpolat", defaultBranch: "main"),
+ Repository(id: "4", name: "statue", owner: "semihpolat", defaultBranch: "main"),
+ Repository(id: "5", name: "littleagents", owner: "semihpolat", defaultBranch: "main"),
+ Repository(id: "6", name: "ultimateaiscraper", owner: "semihpolat", defaultBranch: "main"),
+ ]
+}
+
+// MARK: - Session
+
+struct Session: Identifiable, Hashable {
+ let id: String
+ let title: String
+ let repository: Repository
+ let createdAt: Date
+ var status: SessionStatus
+ var messages: [Message]
+
+ static func == (lhs: Session, rhs: Session) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum SessionStatus: String, Codable {
+ case idle = "Idle"
+ case running = "Running"
+ case completed = "Completed"
+ case error = "Error"
+}
+
+// MARK: - Message
+
+struct Message: Identifiable, Hashable {
+ let id: String
+ let role: MessageRole
+ let content: String
+ let timestamp: Date
+ var toolCalls: [ToolCall]
+
+ static func == (lhs: Message, rhs: Message) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum MessageRole: String, Codable {
+ case user
+ case assistant
+ case system
+}
+
+// MARK: - Tool Call
+
+struct ToolCall: Identifiable, Hashable {
+ let id: String
+ let type: ToolType
+ let name: String
+ let input: String
+ var output: String?
+ var status: ToolCallStatus
+
+ static func == (lhs: ToolCall, rhs: ToolCall) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+enum ToolType: String, Codable {
+ case write = "Write"
+ case bash = "Bash"
+ case read = "Read"
+ case edit = "Edit"
+ case glob = "Glob"
+ case grep = "Grep"
+
+ var color: SwiftUI.Color {
+ switch self {
+ case .write, .edit:
+ return AppTheme.toolWrite
+ case .bash:
+ return AppTheme.toolBash
+ case .read, .glob, .grep:
+ return AppTheme.toolRead
+ }
+ }
+}
+
+enum ToolCallStatus: String, Codable {
+ case pending
+ case running
+ case completed
+ case failed
+}
+
+// MARK: - AI Model
+
+struct AIModel: Identifiable, Hashable {
+ let id: String
+ let name: String
+ let displayName: String
+
+ static let sonnet = AIModel(id: "sonnet-4.5", name: "sonnet", displayName: "Sonnet 4.5")
+ static let opus = AIModel(id: "opus-4.5", name: "opus", displayName: "Opus 4.5")
+ static let haiku = AIModel(id: "haiku", name: "haiku", displayName: "Haiku")
+
+ static let all: [AIModel] = [.sonnet, .opus, .haiku]
+}
+
+// MARK: - Mock Data
+
+extension Session {
+ static let mockList: [Session] = [
+ Session(
+ id: "1",
+ title: "Design Claude Code system architecture for mobile",
+ repository: Repository.mockList[1],
+ createdAt: Date().addingTimeInterval(-3600),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "2",
+ title: "Add popular tweets with images to platform showcase",
+ repository: Repository.mockList[2],
+ createdAt: Date().addingTimeInterval(-7200),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "3",
+ title: "Add popular tweets with images to platform library",
+ repository: Repository.mockList[2],
+ createdAt: Date().addingTimeInterval(-10800),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "4",
+ title: "Evaluate migrating SvelteKit project to Next.js",
+ repository: Repository.mockList[3],
+ createdAt: Date().addingTimeInterval(-14400),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "5",
+ title: "Update resources list with current tools and links",
+ repository: Repository.mockList[4],
+ createdAt: Date().addingTimeInterval(-18000),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "6",
+ title: "Sync fork with original repository",
+ repository: Repository.mockList[3],
+ createdAt: Date().addingTimeInterval(-21600),
+ status: .idle,
+ messages: []
+ ),
+ Session(
+ id: "7",
+ title: "Improve UI Design with Shadcn Style",
+ repository: Repository.mockList[5],
+ createdAt: Date().addingTimeInterval(-25200),
+ status: .idle,
+ messages: []
+ ),
+ ]
+
+ static func mockActiveSession() -> Session {
+ Session(
+ id: "active-1",
+ title: "Add popular tweets with images to platform",
+ repository: Repository.mockList[2],
+ createdAt: Date().addingTimeInterval(-1800),
+ status: .running,
+ messages: [
+ Message(
+ id: "m1",
+ role: .assistant,
+ content: "Haklisın, hemen README.md'yi oluşturup push yapıyorum!",
+ timestamp: Date().addingTimeInterval(-1200),
+ toolCalls: [
+ ToolCall(
+ id: "tc1",
+ type: .write,
+ name: "Write",
+ input: "/home/user/Awesome-Prompts/README.md",
+ output: """
+ + # Awesome AI Prompts
+ +
+ + A curated collection of powerful AI prompts that went viral on X/Twitter.
+ + These prompts have been battle-tested by thousands of users and proven to deliver
+ + exceptional results with ChatGPT, Claude, and other LLMs.
+ +
+ """,
+ status: .completed
+ )
+ ]
+ ),
+ Message(
+ id: "m2",
+ role: .assistant,
+ content: "Şimdi commit ve push yapıyorum:",
+ timestamp: Date().addingTimeInterval(-900),
+ toolCalls: [
+ ToolCall(
+ id: "tc2",
+ type: .bash,
+ name: "Bash",
+ input: "git add README.md && git status",
+ output: nil,
+ status: .completed
+ ),
+ ToolCall(
+ id: "tc3",
+ type: .bash,
+ name: "Bash",
+ input: "git commit -m \"$(cat <<'EOF'\nAdd comprehensive Awesome AI Prompts collection...\nEOF\n)\"",
+ output: nil,
+ status: .completed
+ ),
+ ToolCall(
+ id: "tc4",
+ type: .bash,
+ name: "Bash",
+ input: "git push -u origin claude/add-popular-tweets-dy04J",
+ output: nil,
+ status: .completed
+ )
+ ]
+ ),
+ Message(
+ id: "m3",
+ role: .assistant,
+ content: "Push tamam! Şimdi PR oluşturuyorum:",
+ timestamp: Date().addingTimeInterval(-600),
+ toolCalls: []
+ ),
+ ]
+ )
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Theme/AppTheme.swift b/apps/ios-github-mobile/Sources/Theme/AppTheme.swift
new file mode 100644
index 000000000..2dfa56595
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Theme/AppTheme.swift
@@ -0,0 +1,86 @@
+import SwiftUI
+
+enum AppTheme {
+ // Background colors
+ static let background = Color(red: 0.08, green: 0.08, blue: 0.08)
+ static let surfaceBackground = Color(red: 0.12, green: 0.12, blue: 0.12)
+ static let cardBackground = Color(red: 0.16, green: 0.16, blue: 0.16)
+ static let inputBackground = Color(red: 0.14, green: 0.14, blue: 0.14)
+
+ // Text colors
+ static let primaryText = Color.white
+ static let secondaryText = Color(white: 0.6)
+ static let tertiaryText = Color(white: 0.45)
+
+ // Accent colors
+ static let accent = Color(red: 0.85, green: 0.55, blue: 0.35) // Warm orange like Claude
+ static let accentLight = Color(red: 0.95, green: 0.75, blue: 0.55)
+
+ // Status colors
+ static let success = Color(red: 0.3, green: 0.7, blue: 0.4)
+ static let warning = Color(red: 0.9, green: 0.7, blue: 0.3)
+ static let error = Color(red: 0.9, green: 0.4, blue: 0.4)
+
+ // Tool colors
+ static let toolWrite = Color(red: 0.4, green: 0.7, blue: 0.4)
+ static let toolBash = Color(red: 0.5, green: 0.6, blue: 0.8)
+ static let toolRead = Color(red: 0.7, green: 0.6, blue: 0.8)
+
+ // Chip/Tag colors
+ static let chipBackground = Color(red: 0.2, green: 0.2, blue: 0.2)
+ static let chipBorder = Color(white: 0.3)
+
+ // Button colors
+ static let buttonPrimary = Color(white: 0.95)
+ static let buttonPrimaryText = Color.black
+
+ // Spacing
+ static let spacingXS: CGFloat = 4
+ static let spacingSM: CGFloat = 8
+ static let spacingMD: CGFloat = 12
+ static let spacingLG: CGFloat = 16
+ static let spacingXL: CGFloat = 24
+
+ // Corner radius
+ static let radiusSM: CGFloat = 6
+ static let radiusMD: CGFloat = 10
+ static let radiusLG: CGFloat = 16
+ static let radiusXL: CGFloat = 24
+ static let radiusFull: CGFloat = 100
+}
+
+// MARK: - View Modifiers
+
+struct CardStyle: ViewModifier {
+ func body(content: Content) -> some View {
+ content
+ .background(AppTheme.cardBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusLG))
+ }
+}
+
+struct ChipStyle: ViewModifier {
+ func body(content: Content) -> some View {
+ content
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.primaryText)
+ .padding(.horizontal, AppTheme.spacingMD)
+ .padding(.vertical, AppTheme.spacingSM)
+ .background(AppTheme.chipBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusFull))
+ .overlay(
+ RoundedRectangle(cornerRadius: AppTheme.radiusFull)
+ .stroke(AppTheme.chipBorder, lineWidth: 1)
+ )
+ }
+}
+
+extension View {
+ func cardStyle() -> some View {
+ modifier(CardStyle())
+ }
+
+ func chipStyle() -> some View {
+ modifier(ChipStyle())
+ }
+}
diff --git a/apps/ios-github-mobile/Sources/Views/ChatView.swift b/apps/ios-github-mobile/Sources/Views/ChatView.swift
new file mode 100644
index 000000000..7d6b2b4fe
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Views/ChatView.swift
@@ -0,0 +1,341 @@
+import SwiftUI
+
+struct ChatView: View {
+ @Environment(AppState.self) private var appState
+ let session: Session
+ @State private var messageText = ""
+ @State private var showModelPicker = false
+ @State private var selectedModel: AIModel = .sonnet
+ @State private var showCreatePR = false
+
+ private var currentSession: Session {
+ appState.sessions.first { $0.id == session.id } ?? session
+ }
+
+ var body: some View {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // Messages scroll view
+ ScrollViewReader { proxy in
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: AppTheme.spacingLG) {
+ ForEach(currentSession.messages) { message in
+ MessageView(message: message)
+ .id(message.id)
+ }
+
+ // Create PR button if there are tool calls
+ if currentSession.messages.contains(where: { !$0.toolCalls.isEmpty }) {
+ CreatePRButton(
+ branchName: "claude/add-popular-tweets-dy04J",
+ showCreatePR: $showCreatePR
+ )
+ .padding(.horizontal, AppTheme.spacingLG)
+ }
+ }
+ .padding(.vertical, AppTheme.spacingMD)
+ }
+ .onChange(of: currentSession.messages.count) { _, _ in
+ if let lastMessage = currentSession.messages.last {
+ withAnimation {
+ proxy.scrollTo(lastMessage.id, anchor: .bottom)
+ }
+ }
+ }
+ }
+
+ // Input area
+ VStack(spacing: AppTheme.spacingMD) {
+ MessageInputView(
+ text: $messageText,
+ placeholder: "Add feedback...",
+ onSend: sendMessage
+ )
+
+ // Bottom chips
+ HStack(spacing: AppTheme.spacingSM) {
+ // Branch indicator
+ HStack(spacing: AppTheme.spacingXS) {
+ Text("Branch")
+ .foregroundStyle(AppTheme.secondaryText)
+ Text("claude/add-popular-tweets...")
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ .font(.caption)
+ .padding(.horizontal, AppTheme.spacingMD)
+ .padding(.vertical, AppTheme.spacingSM)
+ .background(AppTheme.chipBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+
+ // Create PR button inline
+ Button {
+ showCreatePR = true
+ } label: {
+ HStack(spacing: AppTheme.spacingXS) {
+ Text("Create PR")
+ .foregroundStyle(AppTheme.primaryText)
+ Image(systemName: "arrow.up.right")
+ .font(.caption)
+ Image(systemName: "ellipsis")
+ .font(.caption2)
+ }
+ }
+ .font(.caption)
+ .padding(.horizontal, AppTheme.spacingMD)
+ .padding(.vertical, AppTheme.spacingSM)
+ .background(AppTheme.chipBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+
+ Spacer()
+ }
+ .padding(.horizontal, AppTheme.spacingMD)
+ }
+ .padding(.bottom, AppTheme.spacingMD)
+ .background(AppTheme.background)
+ }
+ }
+ .navigationTitle(session.repository.fullName)
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .principal) {
+ VStack(spacing: 2) {
+ Text(String(session.title.prefix(30)) + (session.title.count > 30 ? "..." : ""))
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundStyle(AppTheme.primaryText)
+ Text("\(session.repository.fullName) · Default")
+ .font(.caption2)
+ .foregroundStyle(AppTheme.secondaryText)
+ }
+ }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ Menu {
+ Button("View on GitHub") {}
+ Button("Copy branch name") {}
+ Divider()
+ Button("Delete session", role: .destructive) {}
+ } label: {
+ Image(systemName: "ellipsis.circle")
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ }
+ .sheet(isPresented: $showCreatePR) {
+ CreatePRSheet(session: currentSession)
+ .presentationDetents([.medium])
+ }
+ }
+
+ private func sendMessage() {
+ guard !messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+ appState.sendMessage(messageText, to: currentSession)
+ messageText = ""
+ }
+}
+
+// MARK: - Message View
+
+struct MessageView: View {
+ let message: Message
+ @State private var expandedToolCalls: Set = []
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: AppTheme.spacingMD) {
+ // Message content
+ if !message.content.isEmpty {
+ Text(message.content)
+ .font(.body)
+ .foregroundStyle(AppTheme.primaryText)
+ .padding(.horizontal, AppTheme.spacingLG)
+ }
+
+ // Tool calls
+ ForEach(message.toolCalls) { toolCall in
+ ToolCallView(
+ toolCall: toolCall,
+ isExpanded: expandedToolCalls.contains(toolCall.id),
+ onToggle: {
+ if expandedToolCalls.contains(toolCall.id) {
+ expandedToolCalls.remove(toolCall.id)
+ } else {
+ expandedToolCalls.insert(toolCall.id)
+ }
+ }
+ )
+ .padding(.horizontal, AppTheme.spacingLG)
+ }
+ }
+ }
+}
+
+// MARK: - Tool Call View
+
+struct ToolCallView: View {
+ let toolCall: ToolCall
+ let isExpanded: Bool
+ let onToggle: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ // Header
+ Button(action: onToggle) {
+ HStack(spacing: AppTheme.spacingSM) {
+ // Tool type badge
+ Text(toolCall.type.rawValue)
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundStyle(toolCall.type.color)
+
+ // File path or command
+ Text(toolCall.input)
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+ .lineLimit(1)
+
+ Spacer()
+
+ // Expand indicator
+ Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
+ .font(.caption)
+ .foregroundStyle(AppTheme.tertiaryText)
+ }
+ .padding(.horizontal, AppTheme.spacingMD)
+ .padding(.vertical, AppTheme.spacingSM)
+ }
+ .buttonStyle(.plain)
+
+ // Expanded content
+ if isExpanded, let output = toolCall.output {
+ ScrollView(.horizontal, showsIndicators: false) {
+ Text(output)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(AppTheme.secondaryText)
+ .padding(AppTheme.spacingMD)
+ }
+ .frame(maxHeight: 200)
+ .background(AppTheme.surfaceBackground)
+ }
+ }
+ .background(AppTheme.cardBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+ }
+}
+
+// MARK: - Create PR Button
+
+struct CreatePRButton: View {
+ let branchName: String
+ @Binding var showCreatePR: Bool
+
+ var body: some View {
+ HStack {
+ Text(branchName)
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+ .lineLimit(1)
+
+ Spacer()
+
+ Button {
+ showCreatePR = true
+ } label: {
+ HStack(spacing: AppTheme.spacingXS) {
+ Text("Create PR")
+ .fontWeight(.medium)
+ Image(systemName: "arrow.up.right")
+ Image(systemName: "ellipsis")
+ }
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ .padding(AppTheme.spacingMD)
+ .background(AppTheme.cardBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+ }
+}
+
+// MARK: - Create PR Sheet
+
+struct CreatePRSheet: View {
+ @Environment(\.dismiss) private var dismiss
+ let session: Session
+ @State private var prTitle = ""
+ @State private var prDescription = ""
+
+ var body: some View {
+ NavigationStack {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ VStack(spacing: AppTheme.spacingLG) {
+ VStack(alignment: .leading, spacing: AppTheme.spacingSM) {
+ Text("Title")
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+
+ TextField("PR title", text: $prTitle)
+ .textFieldStyle(.plain)
+ .padding(AppTheme.spacingMD)
+ .background(AppTheme.inputBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+ }
+
+ VStack(alignment: .leading, spacing: AppTheme.spacingSM) {
+ Text("Description")
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+
+ TextEditor(text: $prDescription)
+ .scrollContentBackground(.hidden)
+ .padding(AppTheme.spacingMD)
+ .background(AppTheme.inputBackground)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusMD))
+ .frame(minHeight: 120)
+ }
+
+ Spacer()
+
+ Button {
+ // Create PR action
+ dismiss()
+ } label: {
+ Text("Create Pull Request")
+ .font(.body)
+ .fontWeight(.medium)
+ .foregroundStyle(AppTheme.buttonPrimaryText)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, AppTheme.spacingMD)
+ .background(AppTheme.buttonPrimary)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusFull))
+ }
+ }
+ .padding(AppTheme.spacingLG)
+ }
+ .navigationTitle("Create PR")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button("Cancel") {
+ dismiss()
+ }
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ .onAppear {
+ prTitle = session.title
+ }
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ ChatView(session: Session.mockActiveSession())
+ }
+ .environment(AppState())
+ .preferredColorScheme(.dark)
+}
diff --git a/apps/ios-github-mobile/Sources/Views/SessionListView.swift b/apps/ios-github-mobile/Sources/Views/SessionListView.swift
new file mode 100644
index 000000000..229ab0575
--- /dev/null
+++ b/apps/ios-github-mobile/Sources/Views/SessionListView.swift
@@ -0,0 +1,220 @@
+import SwiftUI
+
+struct SessionListView: View {
+ @Environment(AppState.self) private var appState
+ @State private var showingNewSessionSheet = false
+
+ var body: some View {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // Session list
+ ScrollView {
+ LazyVStack(spacing: 0) {
+ // Status indicator
+ HStack {
+ Text("Idle")
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+ Spacer()
+ }
+ .padding(.horizontal, AppTheme.spacingLG)
+ .padding(.top, AppTheme.spacingMD)
+ .padding(.bottom, AppTheme.spacingSM)
+
+ // Sessions
+ ForEach(appState.sessions) { session in
+ SessionRowView(session: session)
+ .onTapGesture {
+ appState.openSession(session)
+ }
+ }
+ }
+ }
+
+ // New session button
+ Button {
+ showingNewSessionSheet = true
+ } label: {
+ Text("New session")
+ .font(.body)
+ .fontWeight(.medium)
+ .foregroundStyle(AppTheme.buttonPrimaryText)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, AppTheme.spacingMD)
+ .background(AppTheme.buttonPrimary)
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.radiusFull))
+ }
+ .padding(.horizontal, AppTheme.spacingLG)
+ .padding(.vertical, AppTheme.spacingMD)
+ .background(AppTheme.background)
+ }
+ }
+ .navigationTitle("Code")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button {
+ // Settings/filter menu
+ } label: {
+ Image(systemName: "line.3.horizontal.decrease")
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ }
+ .sheet(isPresented: $showingNewSessionSheet) {
+ NewSessionSheet()
+ }
+ }
+}
+
+// MARK: - Session Row
+
+struct SessionRowView: View {
+ let session: Session
+
+ var body: some View {
+ HStack(alignment: .center, spacing: AppTheme.spacingMD) {
+ VStack(alignment: .leading, spacing: AppTheme.spacingXS) {
+ Text(session.title)
+ .font(.body)
+ .foregroundStyle(AppTheme.primaryText)
+ .lineLimit(2)
+
+ Text(session.repository.fullName)
+ .font(.subheadline)
+ .foregroundStyle(AppTheme.secondaryText)
+ }
+
+ Spacer()
+
+ Image(systemName: "chevron.right")
+ .font(.footnote)
+ .foregroundStyle(AppTheme.tertiaryText)
+ }
+ .padding(.horizontal, AppTheme.spacingLG)
+ .padding(.vertical, AppTheme.spacingMD)
+ .contentShape(Rectangle())
+ }
+}
+
+// MARK: - New Session Sheet
+
+struct NewSessionSheet: View {
+ @Environment(AppState.self) private var appState
+ @Environment(\.dismiss) private var dismiss
+ @State private var messageText = ""
+ @State private var selectedRepository: Repository?
+ @State private var selectedBranch = "main"
+ @State private var selectedModel: AIModel = .sonnet
+ @State private var showModelPicker = false
+ @State private var showRepoPicker = false
+
+ var body: some View {
+ NavigationStack {
+ ZStack {
+ AppTheme.background.ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ Spacer()
+
+ // Message input area
+ VStack(spacing: AppTheme.spacingMD) {
+ // Text input
+ MessageInputView(
+ text: $messageText,
+ placeholder: "Ask Claude to help with code...",
+ onSend: createSession
+ )
+
+ // Bottom chips
+ HStack(spacing: AppTheme.spacingSM) {
+ // Model picker
+ Button {
+ showModelPicker = true
+ } label: {
+ HStack(spacing: AppTheme.spacingXS) {
+ Image(systemName: "cube")
+ Text(selectedModel.displayName)
+ Image(systemName: "chevron.down")
+ .font(.caption2)
+ }
+ }
+ .chipStyle()
+
+ // Repository picker
+ Button {
+ showRepoPicker = true
+ } label: {
+ HStack(spacing: AppTheme.spacingXS) {
+ Image(systemName: "folder")
+ Text(selectedRepository?.name ?? "Select repo")
+ }
+ }
+ .chipStyle()
+
+ // Branch
+ if selectedRepository != nil {
+ HStack(spacing: AppTheme.spacingXS) {
+ Image(systemName: "arrow.triangle.branch")
+ Text(selectedBranch)
+ }
+ .chipStyle()
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, AppTheme.spacingMD)
+ }
+ .padding(.bottom, AppTheme.spacingLG)
+ }
+ }
+ .navigationTitle("New Session")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button("Cancel") {
+ dismiss()
+ }
+ .foregroundStyle(AppTheme.primaryText)
+ }
+ }
+ .onAppear {
+ selectedRepository = appState.repositories.first
+ }
+ .sheet(isPresented: $showModelPicker) {
+ ModelPickerSheet(selectedModel: $selectedModel)
+ .presentationDetents([.medium])
+ }
+ .sheet(isPresented: $showRepoPicker) {
+ RepositoryPickerSheet(
+ repositories: appState.repositories,
+ selectedRepository: $selectedRepository
+ )
+ .presentationDetents([.medium, .large])
+ }
+ }
+ }
+
+ private func createSession() {
+ guard !messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
+ let repository = selectedRepository else { return }
+
+ let session = appState.createNewSession(
+ title: String(messageText.prefix(50)),
+ repository: repository
+ )
+ appState.sendMessage(messageText, to: session)
+ dismiss()
+ appState.openSession(session)
+ }
+}
+
+#Preview {
+ NavigationStack {
+ SessionListView()
+ }
+ .environment(AppState())
+ .preferredColorScheme(.dark)
+}