From 20a70d5770e16bc38009bc82dfd3a31cd6f33692 Mon Sep 17 00:00:00 2001 From: Andrew Moore Date: Fri, 7 Aug 2026 18:03:28 -0700 Subject: [PATCH] Merge nucleic/zesty-jade-weasel-yb6i into dev --- .../Views/Transcript/SandboxToolDisplay.swift | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/SandboxToolDisplay.swift b/NucleicRemote/NucleicRemote/Views/Transcript/SandboxToolDisplay.swift index 85027d2..b16fb35 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/SandboxToolDisplay.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/SandboxToolDisplay.swift @@ -41,6 +41,10 @@ enum SandboxToolDisplay { case "linux_container": "Linux container" case "nucleic_supervise": "Supervising subagents" case AskUserQuestion.nucleicToolName: "Question for you" + case "nucleic_knowledge_search": "Search project knowledge" + case "nucleic_knowledge_get": "Read project knowledge" + case "nucleic_knowledge_add": "Add project knowledge" + case "nucleic_knowledge_edit": "Edit project knowledge" default: nil } } @@ -71,6 +75,10 @@ enum SandboxToolDisplay { case "linux_vm_control": "Managing the Linux VM…" case "nucleic_supervise": "Waiting on subagents…" case AskUserQuestion.nucleicToolName: "Waiting on your answer…" + case "nucleic_knowledge_search": "Searching project knowledge…" + case "nucleic_knowledge_get": "Reading project knowledge…" + case "nucleic_knowledge_add": "Adding to project knowledge…" + case "nucleic_knowledge_edit": "Editing project knowledge…" default: nil } } @@ -90,6 +98,10 @@ enum SandboxToolDisplay { case "mac_vm_control", "linux_vm_control": "power" case "nucleic_supervise": "eye" case AskUserQuestion.nucleicToolName: "questionmark.bubble" + case "nucleic_knowledge_search": "magnifyingglass" + case "nucleic_knowledge_get": "point.3.connected.trianglepath.dotted" + case "nucleic_knowledge_add": "note.text.badge.plus" + case "nucleic_knowledge_edit": "square.and.pencil" default: nil } } @@ -129,6 +141,16 @@ enum SandboxToolDisplay { let first = questions.first else { return nil } let suffix = questions.count > 1 ? " +\(questions.count - 1) more" : "" return singleLine(first.question) + suffix + // An empty query isn't a degenerate search — it's how the overview is asked for. + case "nucleic_knowledge_search": + return string(input, "query").map(singleLine) ?? "overview" + case "nucleic_knowledge_get": + return entityNames(input) + // Writes are shaped as arrays, so the row is what they add rather than one element of them. + case "nucleic_knowledge_add": + return addSummary(input) + case "nucleic_knowledge_edit": + return editSummary(input) default: return nil } @@ -196,6 +218,46 @@ enum SandboxToolDisplay { return "\(op) — \(subject)" } + /// The entities a knowledge fetch asked for, joined — the names are the call. A lone `name` is + /// accepted too, since fetching one entity is the common case. + private static func entityNames(_ input: JSONValue) -> String? { + let names = (input["names"]?.arrayValue ?? []) + .compactMap { $0.stringValue ?? $0["name"]?.stringValue } + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard !names.isEmpty else { return string(input, "name") } + return clip(names.joined(separator: ", "), limit: 60) + } + + /// What a knowledge write puts into the graph — "2 entities, 3 relations" — counted per bucket. + /// Observations carried inside an entity count toward the observation total, and so do anchors — + /// the schema only ever nests those, so the top-level bucket alone would always read as zero. + private static func addSummary(_ input: JSONValue) -> String? { + let entities = input["entities"]?.arrayValue ?? [] + let nested = entities.reduce(0) { $0 + ($1["observations"]?.arrayValue?.count ?? 0) } + let nestedAnchors = entities.reduce(0) { $0 + ($1["anchors"]?.arrayValue?.count ?? 0) } + let buckets: [(singular: String, plural: String, count: Int)] = [ + ("entity", "entities", entities.count), + ("observation", "observations", + (input["observations"]?.arrayValue?.count ?? 0) + nested), + ("relation", "relations", input["relations"]?.arrayValue?.count ?? 0), + ("anchor", "anchors", + (input["anchors"]?.arrayValue?.count ?? 0) + nestedAnchors), + ] + let parts = buckets.filter { $0.count > 0 } + .map { "\($0.count) \($0.count == 1 ? $0.singular : $0.plural)" } + return parts.isEmpty ? nil : parts.joined(separator: ", ") + } + + /// A knowledge edit as how much it changes — "3 operations" — summed across its buckets, since + /// how many changes land says more in a one-line row than which bucket they came from. + private static func editSummary(_ input: JSONValue) -> String? { + let operations = (input.objectValue ?? [:]).values + .reduce(0) { $0 + ($1.arrayValue?.count ?? 0) } + guard operations > 0 else { return nil } + return "\(operations) operation\(operations == 1 ? "" : "s")" + } + /// A VM lifecycle op as a phrase, so "status" doesn't read as a state label. private static func controlOpPhrase(_ op: String) -> String { switch op {