mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
feat(chat): show active-project indicator in SessionInfoBar + nav title
Adds a visible cue telling the user when their chat is scoped to a Scarf project. Two surfaces: - **SessionInfoBar** gets a folder-fill icon + project name chip at the start of the bar (before the working dot + title). Rendered with `.tint` foregroundStyle so it's visually anchored as the first piece of context. Hidden for non-project chats — the bar looks identical to v2.2.1 when projectName is nil. - **Navigation title** becomes `Chat · <ProjectName>` when scoped, stays as plain `Chat` otherwise. Matches macOS conventions for "subject — detail" titles. ChatViewModel gains two `@Observable` properties: - `currentProjectPath: String?` — absolute path, source of truth for attribution lookups - `currentProjectName: String?` — resolved via the projects registry at session-start; stored to avoid disk reads on every render. Falls back to the raw path (rather than nil) when a session's attribution points at a project no longer in the registry — the user still sees *something* rather than silently losing the indicator. Both are populated in `startACPSession(resume:projectPath:)` from two sources: 1. If the caller passed `projectPath` — fresh project-chat case 2. Otherwise, SessionAttributionService.projectPath(for: resolvedSessionId) — resumed-session case. Means clicking an old project-attributed session from ANY surface (the project's Sessions tab, the global Resume menu) re-surfaces the indicator. When the user starts a non-project session, both fields reset to nil explicitly so the indicator doesn't leak between chats. Files: - ChatViewModel.swift — new properties + resolve logic - SessionInfoBar.swift — new `projectName: String?` parameter + chip rendering - RichChatView.swift — passes chatViewModel.currentProjectName through to SessionInfoBar - ChatView.swift — navTitle reflects the active project 80/80 Swift tests still pass. Visual change only; no test change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,20 @@ final class ChatViewModel {
|
||||
let richChatViewModel: RichChatViewModel
|
||||
private var coordinator: Coordinator?
|
||||
|
||||
/// Absolute project path for the current session, when the chat is
|
||||
/// project-scoped (either started via a project's "New Chat" button
|
||||
/// or resumed from a session that was previously attributed via the
|
||||
/// v2.3 sidecar). Nil for plain global chats. Drives the project
|
||||
/// indicator in SessionInfoBar + the `Chat · <Name>` nav title.
|
||||
private(set) var currentProjectPath: String?
|
||||
|
||||
/// Human-readable name of the active project, resolved from the
|
||||
/// projects registry at session-start time. Stored alongside the
|
||||
/// path so the view renders without hitting disk on every update.
|
||||
/// Nil when `currentProjectPath` is nil OR the path isn't in the
|
||||
/// registry (project was removed after the session was attributed).
|
||||
private(set) var currentProjectName: String?
|
||||
|
||||
// ACP state
|
||||
private var acpClient: ACPClient?
|
||||
private var acpEventTask: Task<Void, Never>?
|
||||
@@ -363,6 +377,37 @@ final class ChatViewModel {
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve which project (if any) this session belongs
|
||||
// to, so SessionInfoBar + nav title can surface it.
|
||||
// Two inputs — use whichever is non-nil:
|
||||
// * `projectPath` — the caller asked for a project
|
||||
// scope (fresh project chat). Just-attributed;
|
||||
// definitely in the sidecar.
|
||||
// * `attribution.projectPath(for: resolvedSessionId)`
|
||||
// — the resumed session was previously attributed.
|
||||
// Covers "click an old project-attributed session
|
||||
// from the global Sessions sidebar / Resume menu"
|
||||
// where projectPath isn't known at the call site.
|
||||
let attributedPath = projectPath
|
||||
?? attribution.projectPath(for: resolvedSessionId)
|
||||
if let path = attributedPath {
|
||||
// Look up a human-readable name from the projects
|
||||
// registry. Missing project (path in the sidecar,
|
||||
// project since removed) → show the path as a
|
||||
// fallback label so the chip still renders and the
|
||||
// user sees *something* rather than silently losing
|
||||
// the indicator.
|
||||
let registry = ProjectDashboardService(context: context).loadRegistry()
|
||||
let name = registry.projects.first(where: { $0.path == path })?.name
|
||||
self.currentProjectPath = path
|
||||
self.currentProjectName = name ?? path
|
||||
} else {
|
||||
// Explicit clear on non-project sessions so the
|
||||
// indicator doesn't leak from a previous chat.
|
||||
self.currentProjectPath = nil
|
||||
self.currentProjectName = nil
|
||||
}
|
||||
|
||||
// Refresh session list so the new ACP session appears in the Resume menu
|
||||
await loadRecentSessions()
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ struct ChatView: View {
|
||||
// push the whole window past the screen. Same pattern as
|
||||
// the Sessions tab fix in the v2.3 branch.
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.navigationTitle("Chat")
|
||||
// v2.3: reflect the active Scarf project in the nav title
|
||||
// so the user can see at a glance that the chat is scoped
|
||||
// (complements the folder chip in SessionInfoBar). Falls
|
||||
// back to the plain "Chat" label for global chats.
|
||||
.navigationTitle(
|
||||
viewModel.currentProjectName.map { "Chat · \($0)" } ?? "Chat"
|
||||
)
|
||||
.task {
|
||||
await viewModel.loadRecentSessions()
|
||||
viewModel.refreshCredentialPreflight()
|
||||
|
||||
@@ -17,7 +17,13 @@ struct RichChatView: View {
|
||||
isWorking: richChat.isAgentWorking,
|
||||
acpInputTokens: richChat.acpInputTokens,
|
||||
acpOutputTokens: richChat.acpOutputTokens,
|
||||
acpThoughtTokens: richChat.acpThoughtTokens
|
||||
acpThoughtTokens: richChat.acpThoughtTokens,
|
||||
// v2.3: surface the active Scarf project (if any) as
|
||||
// a folder chip at the start of the bar. Driven by
|
||||
// ChatViewModel.currentProjectName which is set in
|
||||
// startACPSession on both new project chats and
|
||||
// resumed project-attributed sessions.
|
||||
projectName: chatViewModel.currentProjectName
|
||||
)
|
||||
Divider()
|
||||
|
||||
|
||||
@@ -7,10 +7,28 @@ struct SessionInfoBar: View {
|
||||
var acpInputTokens: Int = 0
|
||||
var acpOutputTokens: Int = 0
|
||||
var acpThoughtTokens: Int = 0
|
||||
/// Name of the Scarf project this session is attributed to, when
|
||||
/// applicable. Nil for plain global chats. Drives the folder-chip
|
||||
/// indicator rendered before the session title. Resolved by
|
||||
/// `ChatViewModel.currentProjectName` — the view just passes it
|
||||
/// through.
|
||||
var projectName: String? = nil
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
if let session {
|
||||
// Project indicator first — visually anchors the session
|
||||
// as "scoped to project X" before the working dot and
|
||||
// title. Hidden for non-project chats so the bar looks
|
||||
// identical to v2.2.1 behavior.
|
||||
if let projectName {
|
||||
Label(projectName, systemImage: "folder.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tint)
|
||||
.lineLimit(1)
|
||||
.help("Chat is scoped to Scarf project \"\(projectName)\"")
|
||||
}
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(isWorking ? .green : .secondary)
|
||||
|
||||
Reference in New Issue
Block a user