mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
M7 #17 (pass-2): empty-transcript UX + defensive project chip
Pass-2 observations:
1. Resumed sessions from Dashboard loaded into chat but showed no
message history.
2. On sessions WITH a project badge, the chat nav-bar chip rendered
the folder icon but no project name.
**Root cause for (1)** — not actually an iOS bug. ACP-native sessions
(the kind ScarfGo starts) don't persist their transcript to the
client-visible `state.db` — only CLI/terminal sessions leave
history there. Confirmed by direct SQLite inspection: the session
IDs in Dashboard's Recent Sessions show `message_count = 0`; the
sessions with lots of messages are all older CLI sessions. The Mac
has this same limitation — just less visible because Mac's Sessions
list surfaces CLI sessions preferentially.
What we fix on the UX side: a friendlier empty state when a resumed
session has no persisted transcript. Replaces the blank canvas with
an icon + "Session resumed" + explanatory caption ("Hermes has the
context for this session, but the transcript isn't cached locally.
Send a message to continue.") Nudges the user toward the right
mental model instead of leaving them wondering why their history
vanished. Gated on `sessionId != nil` so fresh-chat empty state
stays the same.
**Root cause for (2)** — `ProjectEntry.name` shouldn't be empty, but
a defensive treatment avoids ever surfacing a folder-only chip on
edge cases (registry race, partial JSON decode). startResuming now:
- Clears `currentProjectName` eagerly at the start of the resume
flow so a lingering name from a prior session doesn't flash onto
the new header.
- Treats empty strings as nil when the lookup returns one.
And the toolbar renderer adds a `!projectName.isEmpty` guard so an
unexpected empty string never produces an icon-only chip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ struct ChatView: View {
|
||||
VStack(spacing: 1) {
|
||||
Text("Chat")
|
||||
.font(.headline)
|
||||
if let projectName = controller.currentProjectName {
|
||||
if let projectName = controller.currentProjectName, !projectName.isEmpty {
|
||||
Label(projectName, systemImage: "folder.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tint)
|
||||
@@ -157,7 +157,18 @@ struct ChatView: View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
if controller.vm.messages.isEmpty, controller.state == .ready {
|
||||
emptyState
|
||||
if controller.vm.sessionId != nil {
|
||||
// Resumed-session path: session ID is set but
|
||||
// no messages loaded. ACP-native sessions don't
|
||||
// persist their transcript to state.db (only
|
||||
// CLI/terminal sessions do), so resuming one
|
||||
// reconnects to the agent but can't surface
|
||||
// the history client-side. Explain to the user
|
||||
// rather than showing a blank canvas.
|
||||
resumedEmptyState
|
||||
} else {
|
||||
emptyState
|
||||
}
|
||||
}
|
||||
ForEach(controller.vm.messages) { msg in
|
||||
MessageBubble(message: msg)
|
||||
@@ -213,6 +224,32 @@ struct ChatView: View {
|
||||
.padding(.top, 60)
|
||||
}
|
||||
|
||||
/// Friendlier-than-blank state for a session resumed from the
|
||||
/// Dashboard that had no transcript persisted to `state.db`.
|
||||
/// Hermes doesn't write ACP-native session messages to the
|
||||
/// client DB — only CLI/terminal sessions leave a history there —
|
||||
/// so resuming a "recent session" started via Chat means the
|
||||
/// agent has the context but the client can't replay it. The
|
||||
/// user can keep chatting and the agent will have full memory.
|
||||
@ViewBuilder
|
||||
private var resumedEmptyState: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "arrow.clockwise.circle")
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(.tertiary)
|
||||
Text("Session resumed")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Hermes has the context for this session, but the transcript isn't cached locally. Send a message to continue.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 60)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var composer: some View {
|
||||
HStack(alignment: .bottom, spacing: 8) {
|
||||
@@ -619,18 +656,30 @@ final class ChatController {
|
||||
func startResuming(sessionID: String) async {
|
||||
await stop()
|
||||
vm.reset()
|
||||
// Clear eagerly so a lingering project name from a prior
|
||||
// session doesn't flash onto the new header while the
|
||||
// attribution lookup runs.
|
||||
currentProjectName = nil
|
||||
// Resolve the project name for this session (if any) via the
|
||||
// attribution sidecar + project registry. Set BEFORE the ACP
|
||||
// handshake so the nav-bar subtitle is visible the moment the
|
||||
// "Connecting…" overlay disappears. Run off-thread so we
|
||||
// don't block while the SFTP reads happen.
|
||||
// don't block while the SFTP reads happen. Empty-string names
|
||||
// are treated as nil — registry entries should never have
|
||||
// empty names in practice, but guard against a surprise
|
||||
// JSON-decode edge case that would render just a folder icon
|
||||
// with no text (pass-2 bug: user saw exactly that).
|
||||
let ctx = context
|
||||
currentProjectName = await Task.detached {
|
||||
let resolved: String? = await Task.detached {
|
||||
let attribution = SessionAttributionService(context: ctx)
|
||||
guard let path = attribution.projectPath(for: sessionID) else { return nil }
|
||||
let registry = ProjectDashboardService(context: ctx).loadRegistry()
|
||||
return registry.projects.first(where: { $0.path == path })?.name
|
||||
guard let name = registry.projects.first(where: { $0.path == path })?.name,
|
||||
!name.isEmpty
|
||||
else { return nil }
|
||||
return name
|
||||
}.value
|
||||
currentProjectName = resolved
|
||||
|
||||
state = .connecting
|
||||
let client = ACPClient.forIOSApp(
|
||||
|
||||
Reference in New Issue
Block a user