updates
This commit is contained in:
@@ -81,40 +81,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-continue countdown banner
|
||||
if viewModel.isAutoContinuing {
|
||||
HStack(spacing: 12) {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.scaleEffect(0.7)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(ThinkingVerbs.random())
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundColor(.oaiPrimary)
|
||||
Text("Continuing in \(viewModel.autoContinueCountdown)s")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.oaiSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
viewModel.cancelAutoContinue()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.tint(.red)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(Color.blue.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
// Input bar
|
||||
InputBar(
|
||||
text: $viewModel.inputText,
|
||||
@@ -140,6 +106,16 @@ struct ChatView: View {
|
||||
.sheet(isPresented: $viewModel.showSkills) {
|
||||
AgentSkillsView()
|
||||
}
|
||||
.sheet(item: Binding(
|
||||
get: { MCPService.shared.pendingBashCommand },
|
||||
set: { _ in }
|
||||
)) { pending in
|
||||
BashApprovalSheet(
|
||||
pending: pending,
|
||||
onApprove: { forSession in MCPService.shared.approvePendingBashCommand(forSession: forSession) },
|
||||
onDeny: { MCPService.shared.denyPendingBashCommand() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
128
oAI/Views/Screens/BashApprovalSheet.swift
Normal file
128
oAI/Views/Screens/BashApprovalSheet.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// BashApprovalSheet.swift
|
||||
// oAI
|
||||
//
|
||||
// Approval UI for AI-requested bash commands
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Rune Olsen
|
||||
//
|
||||
// This file is part of oAI.
|
||||
//
|
||||
// oAI is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// oAI is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
|
||||
// Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public
|
||||
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BashApprovalSheet: View {
|
||||
let pending: MCPService.PendingBashCommand
|
||||
let onApprove: (_ forSession: Bool) -> Void
|
||||
let onDeny: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
// Header
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "terminal.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.orange)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Allow Shell Command?")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
Text("The AI wants to run the following command")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Command display
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("COMMAND")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
ScrollView {
|
||||
Text(pending.command)
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.foregroundStyle(.primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
.padding(12)
|
||||
}
|
||||
.frame(maxHeight: 180)
|
||||
.background(Color.secondary.opacity(0.08))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
// Working directory
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "folder")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Working directory:")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(pending.workingDirectory)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
// Warning banner
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.font(.system(size: 13))
|
||||
.padding(.top, 1)
|
||||
Text("Shell commands have full access to your system. Only approve commands you understand and trust.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.orange.opacity(0.08))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
// Buttons
|
||||
HStack(spacing: 8) {
|
||||
Button("Deny") {
|
||||
onDeny()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
.keyboardShortcut(.escape, modifiers: [])
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Allow Once") {
|
||||
onApprove(false)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.orange)
|
||||
|
||||
Button("Allow for Session") {
|
||||
onApprove(true)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.orange)
|
||||
.keyboardShortcut(.return, modifiers: [])
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 480)
|
||||
}
|
||||
}
|
||||
@@ -528,6 +528,99 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
}
|
||||
|
||||
// Anytype integration UI hidden (work in progress — see AnytypeMCPService.swift)
|
||||
|
||||
// MARK: Bash Execution
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "terminal.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.orange)
|
||||
Text("Bash Execution")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
}
|
||||
Text("Allow the AI to run shell commands on your machine. Commands are executed via /bin/zsh. Enable approval mode to review each command before it runs.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Status")
|
||||
formSection {
|
||||
row("Enable Bash Execution") {
|
||||
Toggle("", isOn: $settingsService.bashEnabled)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: settingsService.bashEnabled ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(settingsService.bashEnabled ? .orange : .secondary)
|
||||
.font(.system(size: 13))
|
||||
Text(settingsService.bashEnabled ? "Active — AI can run shell commands" : "Disabled")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
if settingsService.bashEnabled {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Settings")
|
||||
formSection {
|
||||
row("Require Approval") {
|
||||
Toggle("", isOn: $settingsService.bashRequireApproval)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
rowDivider()
|
||||
row("Working Directory") {
|
||||
TextField("~", text: $settingsService.bashWorkingDirectory)
|
||||
.textFieldStyle(.plain)
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(width: 200)
|
||||
}
|
||||
rowDivider()
|
||||
row("Timeout (seconds)") {
|
||||
HStack(spacing: 8) {
|
||||
Stepper("", value: $settingsService.bashTimeout, in: 5...300, step: 5)
|
||||
.labelsHidden()
|
||||
Text("\(settingsService.bashTimeout)s")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 36, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if settingsService.bashRequireApproval {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "hand.raised.fill")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.orange)
|
||||
Text("Each command will require your approval before running.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
} else {
|
||||
HStack(alignment: .top, spacing: 6) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.red)
|
||||
.padding(.top, 1)
|
||||
Text("Auto-execute mode: commands run without approval. Use with caution.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Appearance Tab
|
||||
|
||||
Reference in New Issue
Block a user