Files
multica/apps/ios/Multica/Views/Tasks/TaskMessagesView.swift
Jiayuan 31c2476a7b feat(ios): add MVP iOS app with issue management and agent log viewing
SwiftUI app (iOS 17+) with zero third-party dependencies:
- Passwordless email authentication with Keychain token storage
- Workspace selection and multi-workspace support
- Issue list grouped by status with search and filtering
- Issue detail with inline status/priority/assignee editing
- Comments with threaded display and compose
- Agent task runs history and real-time execution log streaming
- WebSocket integration for live updates
2026-04-02 23:50:28 +08:00

139 lines
4.1 KiB
Swift

import SwiftUI
struct TaskMessagesView: View {
let messages: [TaskMessage]
let isLive: Bool
var body: some View {
LazyVStack(alignment: .leading, spacing: 2) {
if messages.isEmpty {
Text("No log messages")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.padding()
}
ForEach(messages) { message in
TaskMessageRow(message: message)
}
if isLive && !messages.isEmpty {
HStack(spacing: 6) {
ProgressView()
.controlSize(.mini)
Text("Streaming...")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
}
}
}
}
struct TaskMessageRow: View {
let message: TaskMessage
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 2) {
switch message.type {
case .text:
textMessage
case .thinking:
thinkingMessage
case .toolUse:
toolUseMessage
case .toolResult:
toolResultMessage
case .error:
errorMessage
}
}
.padding(.horizontal, 12)
.padding(.vertical, 3)
}
private var textMessage: some View {
Text(message.content ?? "")
.font(.caption.monospaced())
.foregroundStyle(.primary)
}
private var thinkingMessage: some View {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Image(systemName: "brain")
.font(.caption2)
Text("Thinking")
.font(.caption2.weight(.medium))
}
.foregroundStyle(.purple.opacity(0.7))
if let content = message.content, !content.isEmpty {
Text(content)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(isExpanded ? nil : 3)
.onTapGesture { isExpanded.toggle() }
}
}
}
private var toolUseMessage: some View {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Image(systemName: "wrench")
.font(.caption2)
Text(message.tool ?? "Tool")
.font(.caption2.weight(.semibold).monospaced())
}
.foregroundStyle(.blue)
if let content = message.content, !content.isEmpty {
Text(content)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(isExpanded ? nil : 2)
.onTapGesture { isExpanded.toggle() }
}
}
}
private var toolResultMessage: some View {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Image(systemName: "arrow.turn.down.left")
.font(.caption2)
Text(message.tool ?? "Result")
.font(.caption2.weight(.medium).monospaced())
}
.foregroundStyle(.green)
if let output = message.output, !output.isEmpty {
Text(output)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(isExpanded ? nil : 3)
.onTapGesture { isExpanded.toggle() }
}
}
}
private var errorMessage: some View {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle")
.font(.caption2)
Text(message.content ?? "Error")
.font(.caption2.monospaced())
}
.foregroundStyle(.red)
}
}