mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
M9 #4.6 (pass-2): Dashboard Overview/Sessions split + chat project bar
Pass-2 feedback bundled into one architectural commit:
1. **Project indicator moved out of the nav-bar principal slot.** The
iPhone nav bar's .principal area gets squeezed to icon-only when
adjacent toolbar buttons exist — the result was a folder icon with
no project-name text, which is worse than no indicator at all. New
`projectContextBar` renders a full-width tinted strip BELOW the
nav bar when a session is project-attributed: "Project chat"
caption + folder icon + full project name. Scrolls away with the
message list. Pattern cribbed from Slack's channel-topic header
and Apple Mail's sender strip.
2. **Dashboard split into Overview + Sessions sub-tabs.** Segmented
picker at the top. Overview = stats + 5 most-recent sessions for
at-a-glance; Sessions = the deeper 25-session list with a project
filter. `See all` button on Overview's Recent Sessions header
switches tabs. Addresses pass-2 complaint: "The dashboard might
need tabs to break it down better."
3. **Project filter on the Sessions sub-tab.** Menu picker (scales
to N projects; segmented doesn't). "All projects" clears; each
project entry filters to sessions attributed there. Uses the same
attribution map loaded once in `IOSDashboardViewModel.load()`, so
filtering is an O(n) in-memory pass over 25 sessions — no extra
SFTP traffic. Addresses pass-2 complaint: "we should add a filter
to the sessions selector in the dash to see by project."
4. **`IOSDashboardViewModel` exposes the wider surface:**
- `allSessions` (25-session window, feeds the Sessions tab)
- `allProjects` (project registry, drives the filter menu)
- `sessions(filteredBy: String?)` helper — accepts a project name
(nil = all), returns filtered subset.
Mac parity note from the earlier commit message still stands — Mac's
global Sessions list doesn't currently filter by project either.
That's a parallel post-TestFlight followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,12 @@ public final class IOSDashboardViewModel {
|
||||
// MARK: - Published state
|
||||
|
||||
public var stats: HermesDataService.SessionStats = .empty
|
||||
/// Recent 5 sessions for the Overview sub-tab (glance-only surface).
|
||||
public var recentSessions: [HermesSession] = []
|
||||
/// Deeper session list for the Sessions sub-tab — larger window +
|
||||
/// filterable by project. Default 25; enough to cover "what did I
|
||||
/// work on this week" without paging.
|
||||
public var allSessions: [HermesSession] = []
|
||||
public var sessionPreviews: [String: String] = [:]
|
||||
public var isLoading: Bool = true
|
||||
|
||||
@@ -39,6 +44,10 @@ public final class IOSDashboardViewModel {
|
||||
/// sessions on screen are attributed.
|
||||
public private(set) var sessionProjectNames: [String: String] = [:]
|
||||
|
||||
/// Every configured project, for the filter picker in the
|
||||
/// Sessions sub-tab. Populated alongside `sessionProjectNames`.
|
||||
public private(set) var allProjects: [ProjectEntry] = []
|
||||
|
||||
/// Surfaced when the SQLite snapshot or DB open fails. Shown in a
|
||||
/// yellow banner above the stats with a "Retry" button. `nil` means
|
||||
/// the last load was healthy.
|
||||
@@ -63,7 +72,8 @@ public final class IOSDashboardViewModel {
|
||||
|
||||
stats = await dataService.fetchStats()
|
||||
recentSessions = await dataService.fetchSessions(limit: 5)
|
||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 5)
|
||||
allSessions = await dataService.fetchSessions(limit: 25)
|
||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 25)
|
||||
|
||||
// Attribution lookup (pass-2 UX): load the session→project
|
||||
// sidecar + project registry once so Dashboard rows can show
|
||||
@@ -72,7 +82,7 @@ public final class IOSDashboardViewModel {
|
||||
// cell. Failure is silent — the absence of project labels is
|
||||
// a cosmetic degradation, not a data-loss problem.
|
||||
let ctx = context
|
||||
let attributions: [String: String] = await Task.detached {
|
||||
let bundle: (names: [String: String], projects: [ProjectEntry]) = await Task.detached {
|
||||
let attribution = SessionAttributionService(context: ctx)
|
||||
let projectRegistry = ProjectDashboardService(context: ctx).loadRegistry()
|
||||
let pathToName = Dictionary(
|
||||
@@ -85,14 +95,28 @@ public final class IOSDashboardViewModel {
|
||||
result[sessionID] = name
|
||||
}
|
||||
}
|
||||
return result
|
||||
return (names: result, projects: projectRegistry.projects)
|
||||
}.value
|
||||
sessionProjectNames = attributions
|
||||
sessionProjectNames = bundle.names
|
||||
allProjects = bundle.projects
|
||||
|
||||
await dataService.close()
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
/// Sessions matching the given project filter. `nil` returns
|
||||
/// all 25 recent sessions (no filtering). `projectName` is the
|
||||
/// ProjectEntry.name that's the key in `sessionProjectNames`, so
|
||||
/// the filter is an O(n) dict lookup per session — cheap at our
|
||||
/// 25-session window. Sorting is preserved (newest first) from
|
||||
/// the upstream `fetchSessions(limit:)` query.
|
||||
public func sessions(filteredBy projectName: String?) -> [HermesSession] {
|
||||
guard let projectName, !projectName.isEmpty else { return allSessions }
|
||||
return allSessions.filter { session in
|
||||
sessionProjectNames[session.id] == projectName
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper used by DashboardView rows. Returns the project display
|
||||
/// name a session is attributed to, or nil for unattributed
|
||||
/// sessions (CLI-started, or started before v2.3).
|
||||
|
||||
Reference in New Issue
Block a user