openclaw/apps/ios-github-mobile/Sources/Views/SessionListView.swift
Claude cffaf0b904
feat: add SwiftUI GitHub mobile app inspired by Claude Code
- Create new iOS app at apps/ios-github-mobile
- Session list view with repository info and status
- Chat view with collapsible tool executions (Write, Bash, Read)
- Message input with model/repo/branch selection chips
- PR creation flow simulation
- Dark theme matching Claude Code aesthetic
- Mock data for repos, sessions, and tool calls
- Modern SwiftUI with @Observable, NavigationStack

https://claude.ai/code/session_018qmTB96S8xvNRQFrhcvzJp
2026-01-23 08:22:15 +00:00

221 lines
7.7 KiB
Swift

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)
}