Merge nucleic/amber-spruce-koala-cxai into dev
This commit is contained in:
@@ -429,6 +429,10 @@ struct SessionDetailView: View {
|
||||
approval: approval, questions: questions, availableHeight: availableHeight)
|
||||
} else {
|
||||
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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// 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
|
||||
@@ -757,6 +768,7 @@ struct TranscriptList: View {
|
||||
// changes as its streaming deltas coalesce — nudging the viewport mid-stream.
|
||||
Color.clear.frame(height: 1).id(Self.bottomAnchorID)
|
||||
}
|
||||
.environment(\.transcriptWorktreeRoot, worktreeRoot)
|
||||
.padding()
|
||||
// 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.
|
||||
|
||||
@@ -454,6 +454,72 @@ enum HostCommandSummary {
|
||||
if raw.contains("git rev-parse --show-toplevel") { return "the repository root" }
|
||||
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
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import SwiftUI
|
||||
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
|
||||
/// 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
|
||||
@@ -108,6 +122,11 @@ struct HostCommandBreakdown: View {
|
||||
/// The chat card heads its row with the purpose already, so it passes `false`.
|
||||
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
|
||||
|
||||
var body: some View {
|
||||
@@ -117,7 +136,7 @@ struct HostCommandBreakdown: View {
|
||||
if summary.isElevated || summary.isDestructive { riskBanner }
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user