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)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user