feat(mac): restructure config settings grid

This commit is contained in:
Peter Steinberger
2025-12-13 15:55:31 +00:00
parent 9ad341d668
commit b3b4013637

View File

@@ -3,6 +3,7 @@ import SwiftUI
@MainActor @MainActor
struct ConfigSettings: View { struct ConfigSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview private let isPreview = ProcessInfo.processInfo.isPreview
private let labelColumnWidth: CGFloat = 120
@State private var configModel: String = "" @State private var configModel: String = ""
@State private var customModel: String = "" @State private var customModel: String = ""
@State private var configSaving = false @State private var configSaving = false
@@ -25,6 +26,7 @@ struct ConfigSettings: View {
@State private var browserAttachOnly: Bool = false @State private var browserAttachOnly: Bool = false
var body: some View { var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
Text("Clawdis CLI config") Text("Clawdis CLI config")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
@@ -32,12 +34,14 @@ struct ConfigSettings: View {
.font(.callout) .font(.callout)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
LabeledContent("Model") { GroupBox("Agent") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Model")
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Picker("Model", selection: self.$configModel) { Picker("Model", selection: self.$configModel) {
ForEach(self.models) { choice in ForEach(self.models) { choice in
Text( Text("\(choice.name)\(choice.provider.uppercased())")
"\(choice.name)\(choice.provider.uppercased())")
.tag(choice.id) .tag(choice.id)
} }
Text("Manual entry…").tag("__custom__") Text("Manual entry…").tag("__custom__")
@@ -52,7 +56,7 @@ struct ConfigSettings: View {
if self.configModel == "__custom__" { if self.configModel == "__custom__" {
TextField("Enter model ID", text: self.$customModel) TextField("Enter model ID", text: self.$customModel)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(width: 320) .frame(width: 360)
.onChange(of: self.customModel) { _, newValue in .onChange(of: self.customModel) { _, newValue in
self.configModel = newValue self.configModel = newValue
self.autosaveConfig() self.autosaveConfig()
@@ -72,8 +76,14 @@ struct ConfigSettings: View {
} }
} }
} }
}
.frame(maxWidth: 520, alignment: .leading)
}
LabeledContent("Heartbeat") { GroupBox("Heartbeat") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Schedule")
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 12) { HStack(spacing: 12) {
Stepper( Stepper(
@@ -83,38 +93,45 @@ struct ConfigSettings: View {
in: 0...720) in: 0...720)
{ {
Text("Every \(self.heartbeatMinutes ?? 10) min") Text("Every \(self.heartbeatMinutes ?? 10) min")
.frame(width: 150, alignment: .leading)
} }
.help("Set to 0 to disable automatic heartbeats") .help("Set to 0 to disable automatic heartbeats")
TextField("HEARTBEAT", text: self.$heartbeatBody) TextField("HEARTBEAT", text: self.$heartbeatBody)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(width: 240) .frame(width: 200)
.onChange(of: self.heartbeatBody) { _, _ in .onChange(of: self.heartbeatBody) { _, _ in
self.autosaveConfig() self.autosaveConfig()
} }
.help("Message body sent on each heartbeat") .help("Message body sent on each heartbeat")
} }
Text("Heartbeats keep Pi sessions warm; 0 minutes disables them.") Text("Heartbeats keep Pi sessions warm; 0 minutes disables them.")
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
}
.frame(maxWidth: 520, alignment: .leading)
}
Divider().padding(.vertical, 4) GroupBox("Web Chat") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
LabeledContent("Web chat") { GridRow {
VStack(alignment: .leading, spacing: 6) { self.gridLabel("Enabled")
Toggle("Enable embedded web chat (loopback only)", isOn: self.$webChatEnabled) Toggle("", isOn: self.$webChatEnabled)
.labelsHidden()
.toggleStyle(.switch) .toggleStyle(.switch)
.frame(width: 320, alignment: .leading) }
HStack(spacing: 8) { GridRow {
Text("Port") self.gridLabel("Port")
TextField("18788", value: self.$webChatPort, formatter: NumberFormatter()) TextField("18788", value: self.$webChatPort, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(width: 100) .frame(width: 100)
.disabled(!self.webChatEnabled) .disabled(!self.webChatEnabled)
} }
GridRow {
Color.clear
.frame(width: self.labelColumnWidth, height: 1)
Text( Text(
""" """
Mac app connects to the gateways loopback web chat on this port. Mac app connects to the gateways loopback web chat on this port.
@@ -122,30 +139,32 @@ struct ConfigSettings: View {
""") """)
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(maxWidth: 480, alignment: .leading) .frame(maxWidth: 360, alignment: .leading)
} }
} }
.frame(maxWidth: 520, alignment: .leading)
}
Divider().padding(.vertical, 4) GroupBox("Browser (clawd)") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
LabeledContent("Browser (clawd)") { GridRow {
VStack(alignment: .leading, spacing: 8) { self.gridLabel("Enabled")
Toggle("Enable clawd browser control", isOn: self.$browserEnabled) Toggle("", isOn: self.$browserEnabled)
.labelsHidden()
.toggleStyle(.switch) .toggleStyle(.switch)
.frame(width: 360, alignment: .leading)
.onChange(of: self.browserEnabled) { _, _ in self.autosaveConfig() } .onChange(of: self.browserEnabled) { _, _ in self.autosaveConfig() }
}
HStack(spacing: 8) { GridRow {
Text("Control URL") self.gridLabel("Control URL")
TextField("http://127.0.0.1:18791", text: self.$browserControlUrl) TextField("http://127.0.0.1:18791", text: self.$browserControlUrl)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(width: 320) .frame(width: 360)
.disabled(!self.browserEnabled) .disabled(!self.browserEnabled)
.onChange(of: self.browserControlUrl) { _, _ in self.autosaveConfig() } .onChange(of: self.browserControlUrl) { _, _ in self.autosaveConfig() }
} }
GridRow {
self.gridLabel("Accent")
HStack(spacing: 8) { HStack(spacing: 8) {
Text("Accent")
TextField("#FF4500", text: self.$browserColorHex) TextField("#FF4500", text: self.$browserColorHex)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(width: 120) .frame(width: 120)
@@ -159,27 +178,35 @@ struct ConfigSettings: View {
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
}
Toggle("Attach only (never launch)", isOn: self.$browserAttachOnly) GridRow {
self.gridLabel("Attach only")
Toggle("", isOn: self.$browserAttachOnly)
.labelsHidden()
.toggleStyle(.switch) .toggleStyle(.switch)
.frame(width: 360, alignment: .leading)
.disabled(!self.browserEnabled) .disabled(!self.browserEnabled)
.onChange(of: self.browserAttachOnly) { _, _ in self.autosaveConfig() } .onChange(of: self.browserAttachOnly) { _, _ in self.autosaveConfig() }
.help("When enabled, the browser server will only connect if the clawd browser is already running.") .help("When enabled, the browser server will only connect if the clawd browser is already running.")
}
GridRow {
Color.clear
.frame(width: self.labelColumnWidth, height: 1)
Text( Text(
"Clawd uses a separate Chrome profile and ports (default 18791/18792) so it wont interfere with your daily browser." "Clawd uses a separate Chrome profile and ports (default 18791/18792) so it wont interfere with your daily browser."
) )
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(maxWidth: 480, alignment: .leading) .frame(maxWidth: 360, alignment: .leading)
} }
} }
.frame(maxWidth: 520, alignment: .leading)
}
Spacer() Spacer(minLength: 0)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12) .padding(12)
}
.onChange(of: self.modelCatalogPath) { _, _ in .onChange(of: self.modelCatalogPath) { _, _ in
Task { await self.loadModels() } Task { await self.loadModels() }
} }
@@ -196,6 +223,12 @@ struct ConfigSettings: View {
} }
} }
private func gridLabel(_ text: String) -> some View {
Text(text)
.foregroundStyle(.secondary)
.frame(width: self.labelColumnWidth, alignment: .leading)
}
private func loadConfig() { private func loadConfig() {
let parsed = self.loadConfigDict() let parsed = self.loadConfigDict()
let inbound = parsed["inbound"] as? [String: Any] let inbound = parsed["inbound"] as? [String: Any]