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

This commit is contained in:
2026-07-17 19:32:38 -07:00
parent f291392900
commit b36a8ead7d
3 changed files with 98 additions and 1 deletions
@@ -429,6 +429,10 @@ struct SessionDetailView: View {
approval: approval, questions: questions, availableHeight: availableHeight) approval: approval, questions: questions, availableHeight: availableHeight)
} else { } else {
ApprovalCardView(approval: approval, availableHeight: availableHeight) ApprovalCardView(approval: approval, availableHeight: availableHeight)
// So a host_exec approval's working-directory line abbreviates the "cd"
// target the same way the settled transcript card does.
.environment(\.transcriptWorktreeRoot,
TranscriptProjection.worktreeRoot(in: store.openEvents[...]))
} }
} else if store.openApprovalLoading { } else if store.openApprovalLoading {
// Opened from a Live Activity / notification for a session blocked on approval, but // Opened from a Live Activity / notification for a session blocked on approval, but
@@ -665,6 +669,13 @@ struct TranscriptList: View {
projectionCache.items(for: events, showRaw: showRaw, showLockEvents: showLockEvents) projectionCache.items(for: events, showRaw: showRaw, showLockEvents: showLockEvents)
} }
/// The session's worktree root — its `sessionStarted` cwd — read straight from the events (an
/// early-returning scan) and injected into the transcript so a host card's working-directory
/// line can abbreviate an absolute "cd" path to a short "<worktree name>/…" anchor.
private var worktreeRoot: String? {
TranscriptProjection.worktreeRoot(in: events[...])
}
/// Kick off (or restart) the off-main Markdown pre-warm for the visible messages. `MarkdownText` /// Kick off (or restart) the off-main Markdown pre-warm for the visible messages. `MarkdownText`
/// parses every prose line with `AttributedString(markdown:)` during the eager first layout — a /// parses every prose line with `AttributedString(markdown:)` during the eager first layout — a
/// cost that lands on the main thread right as the session is pushed and the tab bar slides /// cost that lands on the main thread right as the session is pushed and the tab bar slides
@@ -757,6 +768,7 @@ struct TranscriptList: View {
// changes as its streaming deltas coalesce — nudging the viewport mid-stream. // changes as its streaming deltas coalesce — nudging the viewport mid-stream.
Color.clear.frame(height: 1).id(Self.bottomAnchorID) Color.clear.frame(height: 1).id(Self.bottomAnchorID)
} }
.environment(\.transcriptWorktreeRoot, worktreeRoot)
.padding() .padding()
// Cap the transcript to a readable measure on a wide iPad so lines don't run // Cap the transcript to a readable measure on a wide iPad so lines don't run
// edge-to-edge on a 13-inch screen; a no-op at phone width. // edge-to-edge on a 13-inch screen; a no-op at phone width.
@@ -454,6 +454,72 @@ enum HostCommandSummary {
if raw.contains("git rev-parse --show-toplevel") { return "the repository root" } if raw.contains("git rev-parse --show-toplevel") { return "the repository root" }
return raw return raw
} }
// MARK: - Path abbreviation
/// Abbreviates an absolute filesystem *directory* path for display in a host card, anchoring it
/// at the nearest meaningful directory so a "cd /Users/me/…" reads with context but sheds the
/// noisy base above it. The mobile mirror of the desktop's `HeuristicSummary.abbreviatePath`
/// (NucleicCore) — the remote app links only `NucleicProtocol`, so it's duplicated here:
/// - a path inside a **worktree** (a sibling under the same base as `worktree`, the session's
/// own worktree root) trims to "<worktree name>/<rest>", dropping the ".nucleic/worktrees/"
/// plumbing above it;
/// - 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 (or an already-relative path) is returned unchanged.
///
/// Unlike the desktop, no symlink-canonicalization is done: the remote only ever sees *host*
/// paths over the wire, with no local filesystem to resolve them against.
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>".
if let worktree, !worktree.isEmpty,
let rel = pathComponent(path, under: (worktree 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,
let rel = pathComponent(path, under: (project as NSString).deletingLastPathComponent) {
return rel
}
// Anything else under the user's home shortens to "~".
if let home = homePrefix(worktree ?? project), let rel = pathComponent(path, under: home) {
return "~/" + rel
}
return path
}
/// The repo root inferred from a worktree path — the segment before "/.nucleic/worktrees/…", so
/// ".../proj1/.nucleic/worktrees/amber" → ".../proj1" — used to feed `abbreviatePath` a project
/// anchor the remote doesn't otherwise carry. Nil for a non-default-layout path (a custom
/// `worktreeBase` won't match; the worktree anchor itself still does).
static func repoRoot(forWorktree worktree: String?) -> String? {
guard let worktree, let range = worktree.range(of: "/.nucleic/worktrees/") else { return nil }
return String(worktree[worktree.startIndex..<range.lowerBound])
}
/// The portion of `path` strictly inside directory `base` ("/base/a/b" under "/base" → "a/b"),
/// or nil when `path` isn't within `base`. 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
}
/// The user's home as "/Users/<name>" (or "/home/<name>"), read from `anchor`'s first two
/// components. 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])"
}
} }
// MARK: - Shell lexing // MARK: - Shell lexing
@@ -1,6 +1,20 @@
import SwiftUI import SwiftUI
import NucleicProtocol import NucleicProtocol
private struct TranscriptWorktreeRootKey: EnvironmentKey {
static let defaultValue: String? = nil
}
extension EnvironmentValues {
/// The session's worktree root (its `sessionStarted` cwd), injected by `SessionDetailView` so a
/// host card's working-directory line can abbreviate an absolute "cd" path to a short
/// "<worktree name>/…" anchor. Nil (the default) leaves the directory verbatim.
var transcriptWorktreeRoot: String? {
get { self[TranscriptWorktreeRootKey.self] }
set { self[TranscriptWorktreeRootKey.self] = newValue }
}
}
/// A `mcp__nucleic__host_exec` tool call in the transcript, rendered as a structured card — the /// A `mcp__nucleic__host_exec` tool call in the transcript, rendered as a structured card — the
/// mobile echo of the Mac's `HostExecToolCard`. It leads with the deterministic, command-derived /// mobile echo of the Mac's `HostExecToolCard`. It leads with the deterministic, command-derived
/// purpose (the easy-to-read headline), shows the literal `$ command` beneath it, and reveals the /// purpose (the easy-to-read headline), shows the literal `$ command` beneath it, and reveals the
@@ -108,6 +122,11 @@ struct HostCommandBreakdown: View {
/// 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 worktree root, so the working-directory line ("cd" target) abbreviates to a
/// short "<worktree name>/…" anchor instead of a full absolute path. Injected by
/// `SessionDetailView`; nil leaves the directory verbatim.
@Environment(\.transcriptWorktreeRoot) private var worktreeRoot
private let accent = Palette.accent private let accent = Palette.accent
var body: some View { var body: some View {
@@ -117,7 +136,7 @@ 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 \(HostCommandSummary.abbreviatePath(dir, worktree: worktreeRoot, project: HostCommandSummary.repoRoot(forWorktree: worktreeRoot)))"])
} }
steps steps
} }