From 5ae8db25c3a5c87a952a4e6745b3e71b309cae19 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 24 Apr 2026 01:34:19 +0200 Subject: [PATCH] fix(chat): resume session on coordinator.selectedSessionId, not just pendingProjectChat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a session in the Projects Sessions tab routed to the Chat section (correct — we want interactive resume, not the read-only Sessions browser), but the session didn't actually load and the project chip didn't appear. Root cause: ChatView only observed `coordinator.pendingProjectChat` (for new chats), not `selectedSessionId` (for resumes). Setting the id had no effect because no consumer existed on the Chat side. Every other session-click site in Scarf routes to `.sessions`, and SessionsView consumes selectedSessionId at its `.task` + clears it. Projects is the exception — the whole point of the per-project Sessions tab is to resume chats interactively rather than browse them, so we route to `.chat`. That routing was right; the Chat side just needed to grow the symmetrical consumer. This commit adds two handoff paths in ChatView (mirrors the existing `pendingProjectChat` pattern): - `.task` picks up a selectedSessionId that was set before ChatView mounted (cold-launch handoff from Projects). - `.onChange(of: coord.selectedSessionId)` picks up mid-session navigation (user clicks a session while already in Chat). Both call `viewModel.resumeSession(id)` then clear the coordinator field. The project chip rendering + navTitle update then happen automatically inside ChatViewModel.resumeSession -> startACPSession, which already looks up attribution via SessionAttributionService.projectPath(for: resolvedSessionId) — that plumbing was in from Part B. The bug was entirely in the trigger, not the side-effect. `else if` between pendingProjectChat and selectedSessionId makes precedence explicit — new-chat wins over resume if both are somehow set. In practice only one is ever populated per navigation, but the explicit ordering avoids surprise. No race with SessionsView's own consumer: `coordinator.selectedSection` ensures only one view is rendering at a time, and both consumers clear the field on consume. 93/93 Swift tests still pass. No test change — this is a view- wiring integration fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scarf/Features/Chat/Views/ChatView.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index 4bc10d3..c73e472 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -40,6 +40,19 @@ struct ChatView: View { coordinator.pendingProjectChat = nil viewModel.startNewSession(projectPath: pending) } + // Same story for resume-session handoff: the user clicked + // a session in the Projects Sessions tab (routes to `.chat` + // rather than `.sessions` so the chat actually reopens). + // SessionsView consumes `selectedSessionId` for its own + // routing; Chat now consumes it too. Mutually exclusive at + // any given render because only one section is active per + // `coordinator.selectedSection`. `else if` makes precedence + // explicit — pendingProjectChat (new) outranks + // selectedSessionId (resume) when both are somehow set. + else if let pendingId = coordinator.selectedSessionId { + coordinator.selectedSessionId = nil + viewModel.resumeSession(pendingId) + } } .onChange(of: fileWatcher.lastChangeDate) { Task { await viewModel.loadRecentSessions() } @@ -56,6 +69,17 @@ struct ChatView: View { viewModel.startNewSession(projectPath: projectPath) } } + // Live handoff for resume: user clicked an existing session in + // the Projects Sessions tab while already in the Chat section + // (or switched back to Chat after). Project-chip rendering + // happens automatically inside ChatViewModel.resumeSession -> + // startACPSession via the attribution.projectPath(for:) lookup. + .onChange(of: coord.selectedSessionId) { _, new in + if let sessionId = new { + coordinator.selectedSessionId = nil + viewModel.resumeSession(sessionId) + } + } } /// Banner rendered between the toolbar and the chat area when either