Merge nucleic/hazy-umber-lemur-pmrf into dev
This commit is contained in:
@@ -68,7 +68,7 @@ struct ApprovalCardView: View {
|
||||
// allow: every one must be a deliberate, one-off approval, never granted in a
|
||||
// way that lets the next one through unseen. Mirrors the Mac.
|
||||
if approval.risk != .destructive,
|
||||
approval.toolName != "mcp__nucleic__copy_artifact",
|
||||
!SandboxToolDisplay.isCopyArtifact(approval.toolName),
|
||||
!store.capabilities.allowAlwaysScopes.isEmpty {
|
||||
Menu("Allow always…") {
|
||||
ForEach(store.capabilities.allowAlwaysScopes, id: \.self) { scope in
|
||||
@@ -92,15 +92,24 @@ struct ApprovalCardView: View {
|
||||
|
||||
/// The kinds of detail an approval carries, resolved once so the body and `hasDetail` agree.
|
||||
/// A git commit reads as a structured commit card (subject + Markdown body); a host_exec gate
|
||||
/// lays out its parsed breakdown + exact command; every other tool shows its untruncated detail.
|
||||
/// lays out its parsed breakdown + exact command; copy_artifact labels its two path endpoints;
|
||||
/// every other tool shows its untruncated detail.
|
||||
private enum Detail {
|
||||
case commit(GitCommitSummary.Commit, command: String)
|
||||
case host(HostCommandSummary.Summary, command: String)
|
||||
case copyArtifact(source: String, destination: String)
|
||||
case text(String)
|
||||
case none
|
||||
}
|
||||
|
||||
private var detail: Detail {
|
||||
if SandboxToolDisplay.isCopyArtifact(approval.toolName),
|
||||
let source = approval.input["source"]?.stringValue,
|
||||
!source.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
let destination = approval.input["destination"]?.stringValue,
|
||||
!destination.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return .copyArtifact(source: source, destination: destination)
|
||||
}
|
||||
if let command = approval.input["command"]?.stringValue,
|
||||
let commit = GitCommitSummary.parse(command) {
|
||||
return .commit(commit, command: command)
|
||||
@@ -128,6 +137,8 @@ struct ApprovalCardView: View {
|
||||
HostCommandBreakdown(summary: parsed)
|
||||
Text("Exact command").font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
|
||||
ApprovalDetailBox(text: Self.truncatedForDisplay(command))
|
||||
case .copyArtifact(let source, let destination):
|
||||
CopyArtifactApprovalDetail(source: source, destination: destination)
|
||||
case .text(let text):
|
||||
ApprovalDetailBox(text: Self.truncatedForDisplay(text))
|
||||
case .none:
|
||||
@@ -164,6 +175,52 @@ struct ApprovalCardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The mobile rendering of a `copy_artifact` gate mirrors the Mac: both paths remain verbatim and
|
||||
/// selectable, but the host -> worktree boundary is explicit instead of buried in a JSON object.
|
||||
private struct CopyArtifactApprovalDetail: View {
|
||||
let source: String
|
||||
let destination: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
pathRow(label: "Source on host", systemImage: "desktopcomputer", path: source)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "arrow.down")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.orange)
|
||||
.frame(width: 16)
|
||||
Divider()
|
||||
}
|
||||
|
||||
pathRow(label: "Destination in worktree", systemImage: "folder", path: destination)
|
||||
}
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
private func pathRow(label: String, systemImage: String, path: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: systemImage)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.orange)
|
||||
.frame(width: 16)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(label)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(path)
|
||||
.font(.caption.monospaced())
|
||||
.textSelection(.enabled)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude's plan-mode exit is a review, not an ordinary permission gate. It always pauses even
|
||||
/// when Auto is enabled and offers the same three outcomes as Claude's first-party surface:
|
||||
/// accept the plan, deny it and remain in plan mode, or return concrete revision feedback.
|
||||
|
||||
@@ -24,6 +24,12 @@ enum SandboxToolDisplay {
|
||||
toolName.hasPrefix(mcpPrefix) ? String(toolName.dropFirst(mcpPrefix.count)) : toolName
|
||||
}
|
||||
|
||||
/// Approval cards use this to select the source -> destination rendering while accepting both
|
||||
/// qualified MCP names and bare names from older transcript/relay payloads.
|
||||
static func isCopyArtifact(_ toolName: String) -> Bool {
|
||||
bareName(toolName) == "copy_artifact"
|
||||
}
|
||||
|
||||
/// The human label that replaces the raw tool name in a card headline. Nil for anything this
|
||||
/// file doesn't speak for, so the caller keeps the tool name unchanged.
|
||||
static func label(for toolName: String) -> String? {
|
||||
|
||||
Reference in New Issue
Block a user