- 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
221 lines
7.7 KiB
Swift
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)
|
|
}
|