mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +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
|
let richChatViewModel: RichChatViewModel
|
||||||
private var coordinator: Coordinator?
|
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
|
// ACP state
|
||||||
private var acpClient: ACPClient?
|
private var acpClient: ACPClient?
|
||||||
private var acpEventTask: Task<Void, Never>?
|
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
|
// Refresh session list so the new ACP session appears in the Resume menu
|
||||||
await loadRecentSessions()
|
await loadRecentSessions()
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ struct ChatView: View {
|
|||||||
// push the whole window past the screen. Same pattern as
|
// push the whole window past the screen. Same pattern as
|
||||||
// the Sessions tab fix in the v2.3 branch.
|
// the Sessions tab fix in the v2.3 branch.
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.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 {
|
.task {
|
||||||
await viewModel.loadRecentSessions()
|
await viewModel.loadRecentSessions()
|
||||||
viewModel.refreshCredentialPreflight()
|
viewModel.refreshCredentialPreflight()
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ struct RichChatView: View {
|
|||||||
isWorking: richChat.isAgentWorking,
|
isWorking: richChat.isAgentWorking,
|
||||||
acpInputTokens: richChat.acpInputTokens,
|
acpInputTokens: richChat.acpInputTokens,
|
||||||
acpOutputTokens: richChat.acpOutputTokens,
|
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()
|
Divider()
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,28 @@ struct SessionInfoBar: View {
|
|||||||
var acpInputTokens: Int = 0
|
var acpInputTokens: Int = 0
|
||||||
var acpOutputTokens: Int = 0
|
var acpOutputTokens: Int = 0
|
||||||
var acpThoughtTokens: 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 {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
if let session {
|
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) {
|
HStack(spacing: 4) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(isWorking ? .green : .secondary)
|
.fill(isWorking ? .green : .secondary)
|
||||||
|
|||||||
Reference in New Issue
Block a user