Merge nucleic/amber-spruce-koala-cxai into dev

This commit is contained in:
2026-07-17 19:25:14 -07:00
parent 00a68415b1
commit 2254ff4ffa
5 changed files with 136 additions and 5 deletions
+19 -3
View File
@@ -41,6 +41,10 @@ struct HostExecCard: View {
var reasonAudit: HostExecReasonAudit? = nil var reasonAudit: HostExecReasonAudit? = nil
/// The card's accent `attention` in a pending approval. /// The card's accent `attention` in a pending approval.
var tint: Color? = nil var tint: Color? = nil
/// The session's project and worktree roots, so the working-directory line abbreviates to a
/// short "<worktree name>/" anchor instead of a full absolute path.
var projectRoot: String? = nil
var worktreeRoot: String? = nil
/// Host execution always carries the attention accent: it's never the calm default path. /// Host execution always carries the attention accent: it's never the calm default path.
private var accent: Color { tint ?? palette.attention } private var accent: Color { tint ?? palette.attention }
@@ -61,7 +65,8 @@ struct HostExecCard: View {
// purpose and the program/actions/flags, then the exact command beneath as the // purpose and the program/actions/flags, then the exact command beneath as the
// ground truth the breakdown is derived from. // ground truth the breakdown is derived from.
if let parsed { if let parsed {
HostCommandBreakdown(summary: parsed, accent: accent, showPurpose: true) HostCommandBreakdown(summary: parsed, accent: accent, showPurpose: true,
projectRoot: projectRoot, worktreeRoot: worktreeRoot)
Text("Exact command") Text("Exact command")
.font(.caption2.weight(.semibold)).foregroundStyle(.secondary) .font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
} }
@@ -237,6 +242,10 @@ struct HostExecToolCard: View {
/// Invoked when the user taps **Skip** on this still-running host call abandons the (apparently /// Invoked when the user taps **Skip** on this still-running host call abandons the (apparently
/// stuck) command so the agent gives up on it. `nil` hides the button; set only while running. /// stuck) command so the agent gives up on it. `nil` hides the button; set only while running.
var onSkip: (() -> Void)? = nil var onSkip: (() -> Void)? = nil
/// The session's project and worktree roots, so the working-directory line ("cd" target) shown
/// on expand abbreviates to a short "<worktree name>/" anchor instead of a full absolute path.
var projectRoot: String? = nil
var worktreeRoot: String? = nil
/// In-chat host calls carry the calm transcript accent (the desktop glyph and "Host" tag do /// In-chat host calls carry the calm transcript accent (the desktop glyph and "Host" tag do
/// the distinguishing); the loud `attention` accent is reserved for the pending approval. /// the distinguishing); the loud `attention` accent is reserved for the pending approval.
@@ -276,7 +285,8 @@ struct HostExecToolCard: View {
if expanded { if expanded {
if hasBreakdown, let parsed { if hasBreakdown, let parsed {
Divider().overlay(accent.opacity(0.14)) Divider().overlay(accent.opacity(0.14))
HostCommandBreakdown(summary: parsed, accent: accent, showPurpose: false) HostCommandBreakdown(summary: parsed, accent: accent, showPurpose: false,
projectRoot: projectRoot, worktreeRoot: worktreeRoot)
.padding(.horizontal, 10).padding(.vertical, 8) .padding(.horizontal, 10).padding(.vertical, 8)
} }
if let output, !output.isEmpty { if let output, !output.isEmpty {
@@ -395,6 +405,11 @@ struct HostCommandBreakdown: View {
/// Whether to render the prominent "Likely purpose" row and any `sudo`/destructive banner. /// Whether to render the prominent "Likely purpose" row and any `sudo`/destructive banner.
/// The chat card heads its row with the purpose already, so it passes `false`. /// The chat card heads its row with the purpose already, so it passes `false`.
var showPurpose: Bool = true var showPurpose: Bool = true
/// The session's project root and worktree root, used to abbreviate the working-directory line
/// ("cd" target) so it reads as "<worktree name>/" or "<project>/" instead of a full
/// absolute path. Nil (the default) leaves the directory verbatim.
var projectRoot: String? = nil
var worktreeRoot: String? = nil
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 9) { VStack(alignment: .leading, spacing: 9) {
@@ -403,7 +418,8 @@ struct HostCommandBreakdown: View {
if summary.isElevated || summary.isDestructive { riskBanner } if summary.isElevated || summary.isDestructive { riskBanner }
} }
if let dir = summary.workingDirectory { if let dir = summary.workingDirectory {
detailRow(icon: "folder", lines: ["in \(dir)"]) detailRow(icon: "folder",
lines: ["in \(HeuristicSummary.abbreviatePath(dir, worktree: worktreeRoot, project: projectRoot))"])
} }
steps steps
} }
+3 -1
View File
@@ -1986,7 +1986,9 @@ struct ApprovalBar: View {
// to run on the host, pretty-formatted, instead of the raw `{"command": }` JSON. // to run on the host, pretty-formatted, instead of the raw `{"command": }` JSON.
HostExecCard( HostExecCard(
command: hostExecCommand, toolName: request.toolName, command: hostExecCommand, toolName: request.toolName,
reason: hostExecReason, reasonAudit: reasonAudit, tint: palette.attention) reason: hostExecReason, reasonAudit: reasonAudit, tint: palette.attention,
projectRoot: store.openSession.flatMap { store.project($0.projectID)?.rootPath },
worktreeRoot: store.openSession?.worktreePath)
.task(id: request.id) { .task(id: request.id) {
// Cross-check the agent's reason against the command on-device, so a reason // Cross-check the agent's reason against the command on-device, so a reason
// that doesn't match what the command does is flagged before approval. Runs // that doesn't match what the command does is flagged before approval. Runs
+3 -1
View File
@@ -284,7 +284,9 @@ struct TranscriptRow: View, Equatable {
environment: HostCommandSummary.hostDisplayEnvironment(workingDirectory: cwd)) environment: HostCommandSummary.hostDisplayEnvironment(workingDirectory: cwd))
return HostExecToolCard( return HostExecToolCard(
command: resolved, output: output, finished: finished, isError: result?.isError == true, command: resolved, output: output, finished: finished, isError: result?.isError == true,
onSkip: onSkip) onSkip: onSkip,
projectRoot: store.openSession.flatMap { store.project($0.projectID)?.rootPath },
worktreeRoot: store.openSession?.worktreePath)
} }
/// The pretty card for a *completed* AskUserQuestion whose result records the user's /// The pretty card for a *completed* AskUserQuestion whose result records the user's
+77
View File
@@ -981,6 +981,83 @@ public enum HeuristicSummary {
return out return out
} }
/// Abbreviates an absolute filesystem *directory* path for display in a tool card, anchoring
/// it at the nearest meaningful directory so it reads with context yet sheds the noisy base
/// above it the rule behind every "cd /Users/me/" a host card would otherwise print in full:
/// - a path inside a **worktree** (a sibling under the same base as `worktree`, the session's
/// own worktree root) trims to "<worktree name>/<rest>", so
/// "/Users/me/proj/.nucleic/worktrees/amber-koala/Sources" "amber-koala/Sources": the
/// per-session worktree name is the anchor and the ".nucleic/worktrees/" plumbing is dropped;
/// - otherwise a path inside the **project** (`project`, the repo root) trims to
/// "<project dir>/<rest>", so "/Users/me/projects/proj1/subdir" "proj1/subdir";
/// - otherwise a path under the user's home shortens to "~/<rest>";
/// - anything else (a path outside all of these, or one that's already relative) is unchanged.
///
/// `worktree`/`project` are the same roots the tool rows already carry (the session's
/// `worktreePath` and the project's `rootPath`); either may be nil, and a rule is skipped when
/// its anchor is missing. The worktree rule is checked first so a worktree path anchors at its
/// short name rather than the ".nucleic/worktrees/" form the project rule would produce.
public static func abbreviatePath(_ path: String, worktree: String?, project: String?) -> String {
guard path.hasPrefix("/") else { return path }
// A worktree lives at "<base>/<name>"; anchoring at that parent base catches the session's
// own worktree *and* any sibling one, keeping "<name>/<rest>" while shedding the shared
// ".nucleic/worktrees/" prefix above it.
if let worktree, !worktree.isEmpty {
for root in canonicalVariants(worktree) {
if let rel = pathComponent(path, under: (root as NSString).deletingLastPathComponent) {
return rel
}
}
}
// A path inside the repo keeps the project's own folder name as its anchor.
if let project, !project.isEmpty {
for root in canonicalVariants(project) {
if let rel = pathComponent(path, under: (root as NSString).deletingLastPathComponent) {
return rel
}
}
}
// Anything else under the user's home shortens to "~". The home prefix is derived from an
// anchor (so it matches the *host* home the paths use, even when the app is itself sandboxed
// under a different home) rather than the process's own `NSHomeDirectory()`.
if let home = homePrefix(worktree ?? project), let rel = pathComponent(path, under: home) {
return "~/" + rel
}
return path
}
/// The portion of `path` strictly inside directory `base` ("/base/a/b" under "/base" "a/b"),
/// or nil when `path` isn't within `base`. `base` must be an absolute directory; the root "/"
/// and an empty base are rejected so they can't swallow every path into a bare component.
private static func pathComponent(_ path: String, under base: String) -> String? {
guard base.hasPrefix("/"), base != "/", !base.isEmpty else { return nil }
let prefix = base.hasSuffix("/") ? base : base + "/"
guard path.hasPrefix(prefix) else { return nil }
let rel = String(path.dropFirst(prefix.count))
return rel.isEmpty ? nil : rel
}
/// A path paired with its symlink-resolved form, deduped so a prefix match holds whether the
/// stored path is the symlinked or the real one (a worktree is reachable both ways).
private static func canonicalVariants(_ path: String) -> [String] {
let canonical = GitWorktreeManager.canonical(path)
return canonical == path ? [path] : [path, canonical]
}
/// The user's home as "/Users/&lt;name&gt;" (or "/home/&lt;name&gt;"), read from `anchor`'s first two
/// components rather than the process's `NSHomeDirectory()` the displayed paths are *host*
/// paths, and this app may itself run sandboxed under a different home. Nil when `anchor` (or
/// nil) doesn't look like a per-user home path.
private static func homePrefix(_ anchor: String?) -> String? {
guard let anchor else { return nil }
let comps = anchor.split(separator: "/", omittingEmptySubsequences: true)
guard comps.count >= 2, comps[0] == "Users" || comps[0] == "home" else { return nil }
return "/\(comps[0])/\(comps[1])"
}
/// The index of the last user message (start of the most recent turn). /// The index of the last user message (start of the most recent turn).
private static func lastTurnStart(_ events: [AgentEvent]) -> Int { private static func lastTurnStart(_ events: [AgentEvent]) -> Int {
var start = events.startIndex var start = events.startIndex
@@ -319,6 +319,40 @@ struct IntelligenceTests {
#expect(HeuristicSummary.displayPath("/repo/file.swift", relativeTo: nil) == "file.swift") #expect(HeuristicSummary.displayPath("/repo/file.swift", relativeTo: nil) == "file.swift")
} }
@Test func abbreviatePathAnchorsAtWorktreeProjectOrHome() {
let project = "/Users/uname/projects/proj1"
let worktree = "/Users/uname/projects/proj1/.nucleic/worktrees/amber-koala"
// A path inside the session's worktree anchors at the worktree name, dropping the
// ".nucleic/worktrees/" plumbing above it.
#expect(HeuristicSummary.abbreviatePath(
worktree + "/Sources/x.swift", worktree: worktree, project: project)
== "amber-koala/Sources/x.swift")
// The worktree root itself just its name.
#expect(HeuristicSummary.abbreviatePath(worktree, worktree: worktree, project: project)
== "amber-koala")
// A *sibling* worktree (same base, different name) anchors at its own name too.
#expect(HeuristicSummary.abbreviatePath(
"/Users/uname/projects/proj1/.nucleic/worktrees/blue-otter/a",
worktree: worktree, project: project) == "blue-otter/a")
// A path in the project but outside any worktree keeps the project folder as the anchor.
#expect(HeuristicSummary.abbreviatePath(
project + "/subdir", worktree: worktree, project: project) == "proj1/subdir")
#expect(HeuristicSummary.abbreviatePath(
"/Users/uname/projects/proj1/subdir", worktree: nil, project: project) == "proj1/subdir")
// Elsewhere under home "~/"; fully outside verbatim; relative unchanged.
#expect(HeuristicSummary.abbreviatePath(
"/Users/uname/Downloads/x", worktree: worktree, project: project) == "~/Downloads/x")
#expect(HeuristicSummary.abbreviatePath(
"/opt/tools/bin", worktree: worktree, project: project) == "/opt/tools/bin")
#expect(HeuristicSummary.abbreviatePath(
"the repository root", worktree: worktree, project: project) == "the repository root")
// No anchors: an absolute path is left as-is.
#expect(HeuristicSummary.abbreviatePath("/x/y", worktree: nil, project: nil) == "/x/y")
}
@Test func aiEligibilityNeverIncludesReadAndRespectsScope() { @Test func aiEligibilityNeverIncludesReadAndRespectsScope() {
let reads = [ let reads = [
ToolCall(toolCallID: "1", name: "Read", input: ["file_path": "/r/A.swift"]), ToolCall(toolCallID: "1", name: "Read", input: ["file_path": "/r/A.swift"]),