* Add multi-user auth and per-user control sessions * Add mission store abstraction and auth UX polish * Fix unused warnings in tooling * Fix Bugbot review issues - Prevent username enumeration by using generic error message - Add pagination support to InMemoryMissionStore::list_missions - Improve config error when JWT_SECRET missing but DASHBOARD_PASSWORD set * Trim stored username in comparison for consistency * Fix mission cleanup to also remove orphaned tree data * Refactor Open Agent as OpenCode workspace host * Remove chromiumoxide and pin @types/react * Pin idna_adapter for MSRV compatibility * Add host-mcp bin target * Use isolated Playwright MCP sessions * Allow Playwright MCP as root * Fix iOS dashboard warnings * Add autoFocus to username field in multi-user login mode Mirrors the iOS implementation behavior where username field is focused when multi-user auth mode is active. * Fix Bugbot review issues - Add conditional ellipsis for tool descriptions (only when > 32 chars) - Add serde(default) to JWT usr field for backward compatibility * Fix empty user ID fallback in multi-user auth Add effective_user_id helper that falls back to username when id is empty, preventing session sharing and token verification issues. * Fix parallel mission history preservation Load existing mission history into runner before starting parallel execution to prevent losing conversation context. * Fix desktop stream controls layout overflow on iPad - Add frame(maxWidth: .infinity) constraints to ensure controls stay within bounds on wide displays - Add alignment: .leading to VStacks for consistent layout - Add Spacer() to buttons row to prevent spreading - Increase label width to 55 for consistent FPS/Quality alignment - Add alignment: .trailing to value text frames * Fix queued user messages not persisted to mission history When a user message was queued (sent while another task was running), it was not being added to the history or persisted to the database. This caused queued messages to be lost from mission history. Added the same persistence logic used for initial messages to the queued message handling code path.
164 lines
6.5 KiB
Swift
164 lines
6.5 KiB
Swift
//
|
|
// ToolUIDataTableView.swift
|
|
// OpenAgentDashboard
|
|
//
|
|
// SwiftUI renderer for ui_dataTable tool
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ToolUIDataTableView: View {
|
|
let table: ToolUIDataTable
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Title header
|
|
if let title = table.title {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "tablecells")
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.accent)
|
|
|
|
Text(title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(Theme.textPrimary)
|
|
|
|
Spacer()
|
|
|
|
// Row count badge
|
|
Text("\(table.rows.count) rows")
|
|
.font(.caption2)
|
|
.foregroundStyle(Theme.textTertiary)
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 12)
|
|
.background(Theme.backgroundSecondary)
|
|
}
|
|
|
|
// Debug info if no columns
|
|
if table.columns.isEmpty {
|
|
Text("No columns defined")
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted)
|
|
.padding()
|
|
} else {
|
|
// Table content with horizontal scroll
|
|
ScrollView(.horizontal, showsIndicators: true) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Header row
|
|
HStack(spacing: 0) {
|
|
ForEach(table.columns, id: \.id) { column in
|
|
Text(column.displayLabel)
|
|
.font(.caption2.weight(.bold))
|
|
.foregroundStyle(Theme.textMuted)
|
|
.textCase(.uppercase)
|
|
.lineLimit(2)
|
|
.frame(width: columnWidth(for: column), alignment: .leading)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
.background(Color.white.opacity(0.03))
|
|
|
|
Rectangle()
|
|
.fill(Theme.border)
|
|
.frame(height: 0.5)
|
|
|
|
// Data rows
|
|
if table.rows.isEmpty {
|
|
Text("No data")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textMuted)
|
|
.padding()
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
ForEach(Array(table.rows.enumerated()), id: \.offset) { index, row in
|
|
HStack(spacing: 0) {
|
|
ForEach(Array(table.columns.enumerated()), id: \.element.id) { colIndex, column in
|
|
let cellValue = getCellValue(row: row, columnId: column.id)
|
|
|
|
Text(cellValue)
|
|
.font(.caption)
|
|
.foregroundStyle(colIndex == 0 ? Theme.textPrimary : Theme.textSecondary)
|
|
.fontWeight(colIndex == 0 ? .medium : .regular)
|
|
.lineLimit(3)
|
|
.frame(width: columnWidth(for: column), alignment: .leading)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 10)
|
|
}
|
|
}
|
|
.background(index % 2 == 0 ? Color.clear : Color.white.opacity(0.02))
|
|
|
|
if index < table.rows.count - 1 {
|
|
Rectangle()
|
|
.fill(Theme.border.opacity(0.3))
|
|
.frame(height: 0.5)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(minWidth: totalTableWidth)
|
|
}
|
|
}
|
|
}
|
|
.background(Theme.backgroundSecondary.opacity(0.5))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
.stroke(Theme.border, lineWidth: 0.5)
|
|
)
|
|
}
|
|
|
|
private func getCellValue(row: [String: AnyCodable], columnId: String) -> String {
|
|
// Try exact match first
|
|
if let value = row[columnId] {
|
|
return value.stringValue
|
|
}
|
|
// Try case-insensitive match
|
|
let lowerId = columnId.lowercased()
|
|
for (key, value) in row {
|
|
if key.lowercased() == lowerId {
|
|
return value.stringValue
|
|
}
|
|
}
|
|
return "-"
|
|
}
|
|
|
|
private var totalTableWidth: CGFloat {
|
|
table.columns.reduce(0) { $0 + columnWidth(for: $1) + 20 } // 20 for padding
|
|
}
|
|
|
|
private func columnWidth(for column: ToolUIDataTable.Column) -> CGFloat {
|
|
// Parse width if provided, otherwise use adaptive default
|
|
if let width = column.width {
|
|
if width.hasSuffix("px") {
|
|
let numStr = width.dropLast(2)
|
|
if let num = Double(numStr) {
|
|
return min(200, max(80, CGFloat(num)))
|
|
}
|
|
}
|
|
if let num = Double(width) {
|
|
return min(200, max(80, CGFloat(num)))
|
|
}
|
|
}
|
|
// Smart default based on column id
|
|
let id = column.id.lowercased()
|
|
if id.contains("name") || id.contains("model") || id.contains("description") {
|
|
return 140
|
|
} else if id.contains("id") || id.contains("cost") || id.contains("price") {
|
|
return 90
|
|
}
|
|
return 110
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
VStack {
|
|
// Preview would go here
|
|
Text("Data Table Preview")
|
|
}
|
|
.padding()
|
|
.background(Theme.backgroundPrimary)
|
|
}
|