feat(mac): add agent-controlled Canvas panel

This commit is contained in:
Peter Steinberger
2025-12-12 19:54:01 +00:00
parent c0abab226d
commit 27a7d9f9d1
14 changed files with 1237 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ struct DebugSettings: View {
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
@AppStorage(canvasEnabledKey) private var canvasEnabled: Bool = true
@State private var modelsCount: Int?
@State private var modelsLoading = false
@State private var modelsError: String?
@@ -25,6 +26,13 @@ struct DebugSettings: View {
@AppStorage(webChatSwiftUIEnabledKey) private var webChatSwiftUIEnabled: Bool = false
@AppStorage(attachExistingGatewayOnlyKey) private var attachExistingGatewayOnly: Bool = false
@State private var canvasSessionKey: String = "main"
@State private var canvasStatus: String?
@State private var canvasError: String?
@State private var canvasEvalJS: String = "document.title"
@State private var canvasEvalResult: String?
@State private var canvasSnapshotPath: String?
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 10) {
@@ -260,6 +268,84 @@ struct DebugSettings: View {
}
.buttonStyle(.bordered)
Divider()
VStack(alignment: .leading, spacing: 8) {
Text("Canvas")
.font(.caption.weight(.semibold))
Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled)
.toggleStyle(.switch)
.help("When off, agent Canvas requests return “Canvas disabled by user”. Manual debug actions still work.")
HStack(spacing: 8) {
TextField("Session", text: self.$canvasSessionKey)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(width: 160)
Button("Show panel") {
Task { await self.canvasShow() }
}
.buttonStyle(.borderedProminent)
Button("Hide panel") {
CanvasManager.shared.hideAll()
self.canvasStatus = "hidden"
self.canvasError = nil
}
.buttonStyle(.bordered)
Button("Write sample page") {
Task { await self.canvasWriteSamplePage() }
}
.buttonStyle(.bordered)
}
HStack(spacing: 8) {
TextField("Eval JS", text: self.$canvasEvalJS)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(maxWidth: 420)
Button("Eval") {
Task { await self.canvasEval() }
}
.buttonStyle(.bordered)
Button("Snapshot") {
Task { await self.canvasSnapshot() }
}
.buttonStyle(.bordered)
}
if let canvasStatus {
Text(canvasStatus)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
if let canvasEvalResult {
Text("eval → \(canvasEvalResult)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
.textSelection(.enabled)
}
if let canvasSnapshotPath {
HStack(spacing: 8) {
Text("snapshot → \(canvasSnapshotPath)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.textSelection(.enabled)
Button("Reveal") {
NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)])
}
.buttonStyle(.bordered)
}
}
if let canvasError {
Text(canvasError)
.font(.caption2)
.foregroundStyle(.red)
} else {
Text("Tip: the session directory is returned by “Show panel”.")
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
LabeledContent("Icon override") {
Picker("Icon override", selection: self.bindingOverride) {
ForEach(IconOverrideSelection.allCases) { option in
@@ -451,6 +537,117 @@ struct DebugSettings: View {
.appendingPathComponent(".clawdis")
.appendingPathComponent("clawdis.json")
}
// MARK: - Canvas debug actions
@MainActor
private func canvasShow() async {
self.canvasError = nil
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
do {
let dir = try CanvasManager.shared.show(sessionKey: session.isEmpty ? "main" : session, path: "/")
self.canvasStatus = "dir: \(dir)"
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasWriteSamplePage() async {
self.canvasError = nil
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
do {
let dir = try CanvasManager.shared.show(sessionKey: session.isEmpty ? "main" : session, path: "/")
let url = URL(fileURLWithPath: dir).appendingPathComponent("index.html", isDirectory: false)
let now = ISO8601DateFormatter().string(from: Date())
let html = """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas Debug</title>
<style>
:root { color-scheme: dark; }
html,body { height:100%; margin:0; background:#0b1020; color:#e5e7eb; }
body { font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
.wrap { padding:16px; }
.row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
.pill { padding:6px 10px; border-radius:999px; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.12); }
button { background:#22c55e; color:#04110a; border:0; border-radius:10px; padding:8px 10px; font-weight:700; cursor:pointer; }
button:active { transform: translateY(1px); }
.panel { margin-top:14px; padding:14px; border-radius:14px; background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.1); }
.grid { display:grid; grid-template-columns: repeat(12, 1fr); gap:10px; margin-top:12px; }
.box { grid-column: span 4; height:80px; border-radius:14px; background: linear-gradient(135deg, rgba(59,130,246,.35), rgba(168,85,247,.25)); border:1px solid rgba(255,255,255,.12); }
.muted { color: rgba(229,231,235,.7); }
</style>
</head>
<body>
<div class="wrap">
<div class="row">
<div class="pill">Canvas Debug</div>
<div class="pill muted">generated: \(now)</div>
<div class="pill muted">userAgent: <span id="ua"></span></div>
<button id="btn">Click me</button>
<div class="pill">count: <span id="count">0</span></div>
</div>
<div class="panel">
<div class="muted">This is a local file served by the WKURLSchemeHandler.</div>
<div class="grid">
<div class="box"></div><div class="box"></div><div class="box"></div>
<div class="box"></div><div class="box"></div><div class="box"></div>
</div>
</div>
</div>
<script>
document.getElementById('ua').textContent = navigator.userAgent;
let n = 0;
document.getElementById('btn').addEventListener('click', () => {
n++;
document.getElementById('count').textContent = String(n);
document.title = 'Canvas Debug (' + n + ')';
});
</script>
</body>
</html>
"""
try html.write(to: url, atomically: true, encoding: .utf8)
self.canvasStatus = "wrote: \(url.path)"
try CanvasManager.shared.goto(sessionKey: session.isEmpty ? "main" : session, path: "/")
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasEval() async {
self.canvasError = nil
self.canvasEvalResult = nil
do {
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let result = try await CanvasManager.shared.eval(
sessionKey: session.isEmpty ? "main" : session,
javaScript: self.canvasEvalJS)
self.canvasEvalResult = result
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasSnapshot() async {
self.canvasError = nil
self.canvasSnapshotPath = nil
do {
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let path = try await CanvasManager.shared.snapshot(
sessionKey: session.isEmpty ? "main" : session,
outPath: nil)
self.canvasSnapshotPath = path
} catch {
self.canvasError = error.localizedDescription
}
}
}
#if DEBUG