Files
maksarsanjeev 52e1e8288b Security + upstream fixes
- Network core moves to a root-only folder; BroV talks to a narrow root helper (netctl.py) over a user-only socket; install script verifies the mihomo SHA256 and migrates keys (scripts/netcore)
- Hardened runtime, no get-task-allow; bypassPermissions removed from the chat; concealed clipboard items are not restored; DangerCheck knows core, LaunchAgents and hook paths; dropped-file copies expire after 7 days
- Ported from upstream Coucou: 1h crash fix (d05f22b), safe settings.json writes (918d30e), Escape/fold for pending approvals (6012900, 40e3ba8), auto-close delay + reopen (74984f2, ea244a7), full AskUserQuestion (52b1562)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:49:11 +03:00

93 lines
3.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - AskUserQuestion model (Foundation-only, testable)
struct AskQuestionOption: Equatable {
var label: String
var description: String
}
struct AskQuestionItem: Equatable {
var question: String // full question text
var header: String // ≤12 chars; used as section label
var options: [AskQuestionOption] // 2–4 choices
var multiSelect: Bool
}
/// Island height of the pending question, readable from the nonisolated `islandSize`. Written on the main actor only.
enum QuestionLayout {
nonisolated(unsafe) static var height: CGFloat?
}
extension AskQuestionItem {
/// True when at least one option carries a description: the card then lists options vertically.
var hasDescriptions: Bool { options.contains { !$0.description.isEmpty } }
}
struct AskQuestion: Equatable {
var questions: [AskQuestionItem] // 1–4 questions
/// Island height that fits the tallest question without truncation (rough estimate, text wraps at ~500 pt).
var estimatedIslandHeight: CGFloat {
func lines(_ text: String, charWidth: CGFloat) -> CGFloat {
max(1, (CGFloat(text.count) * charWidth / 500).rounded(.up))
}
let tallest = questions.map { item -> CGFloat in
var h: CGFloat = 20 + lines(item.question, charWidth: 7) * 17 + 64
if !item.header.isEmpty { h += 14 }
if item.hasDescriptions {
for opt in item.options {
h += 34 + (opt.description.isEmpty ? 0 : lines(opt.description, charWidth: 6.4) * 14)
}
h += 40 // "Другое…" row
} else {
h += item.options.count >= 3 ? 74 : 40
}
if item.multiSelect { h += 34 }
return h
}.max() ?? 160
return min(max(tallest, 160), 560)
}
// MARK: - Parse from tool_input dict
// Returns nil if the payload is malformed (fallback → Allow/Deny card).
static func parse(toolInput: [String: Any]) -> AskQuestion? {
guard let rawQs = toolInput["questions"] as? [[String: Any]],
!rawQs.isEmpty, rawQs.count <= 4 else { return nil }
var items: [AskQuestionItem] = []
for raw in rawQs {
guard let question = raw["question"] as? String, !question.isEmpty,
let rawOpts = raw["options"] as? [[String: Any]],
rawOpts.count >= 2, rawOpts.count <= 4 else { return nil }
var opts: [AskQuestionOption] = []
for opt in rawOpts {
guard let label = opt["label"] as? String, !label.isEmpty else { return nil }
let desc = opt["description"] as? String ?? ""
opts.append(AskQuestionOption(label: label, description: desc))
}
let rawHeader = raw["header"] as? String ?? ""
let header = String(rawHeader.prefix(12))
let multi = raw["multiSelect"] as? Bool ?? false
items.append(AskQuestionItem(question: question, header: header, options: opts, multiSelect: multi))
}
return AskQuestion(questions: items)
}
// MARK: - Build answers dict
// selections[i] = list of labels selected for question[i].
// Single-select / Other…: one label → String value (Claude Code 2.1.85+).
// Multi-select: array of labels → [String] value (Claude Code 2.1.136+).
static func buildAnswers(questions: [AskQuestionItem], selections: [[String]]) -> [String: Any] {
var answers: [String: Any] = [:]
for (i, item) in questions.enumerated() {
guard i < selections.count, !selections[i].isEmpty else { continue }
if item.multiSelect {
answers[item.question] = selections[i] // array for multi-select
} else {
answers[item.question] = selections[i][0] // string for single-select
}
}
return answers
}
}