mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
fix(ios-chat): surface project context block write failures
ChatController.resetAndStartInProject swallowed the SFTP write of the Scarf-managed AGENTS.md block via `try?` inside `Task.detached`. On failure (permission denied, SFTP error, malformed path) the user saw no feedback while the UI continued claiming the session was project-scoped — but the agent never received the project context, leading to silently degraded chat quality. Replace the `try? + fire-and-forget` with a `Result`-returning detached task. On `.failure`, log the underlying error via `os.Logger` and route it to the existing ACP error banner (`acpError` / `acpErrorHint` / `acpErrorDetails`) with a friendly "Project context not written — agent will proceed without it" payload. Session still starts; only the context-augmentation step is reported as missing. The session-attribution write at the same flow stays fire-and-forget by design — `SessionAttributionService.persist` already logs failures internally, and a missed attribution is purely cosmetic (Dashboard project-badge cosmetics, not chat function). Replaced the comment to make that intent explicit so future readers don't accidentally "fix" it by promoting attribution failures to the chat banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import SwiftUI
|
||||
import ScarfCore
|
||||
import ScarfIOS
|
||||
import os
|
||||
|
||||
// The Chat feature on iOS is gated on `canImport(SQLite3)` because
|
||||
// `RichChatViewModel` reads session history from `HermesDataService`
|
||||
@@ -445,6 +446,11 @@ final class ChatController {
|
||||
private var client: ACPClient?
|
||||
private var eventTask: Task<Void, Never>?
|
||||
|
||||
private static let logger = Logger(
|
||||
subsystem: "com.scarf.ios",
|
||||
category: "ChatController"
|
||||
)
|
||||
|
||||
init(context: ServerContext) {
|
||||
self.context = context
|
||||
self.vm = RichChatViewModel(context: context)
|
||||
@@ -572,20 +578,36 @@ final class ChatController {
|
||||
vm.reset()
|
||||
currentProjectName = project.name
|
||||
// Write the context block first. Non-fatal on failure — chat
|
||||
// still starts, just without the managed block; the user sees
|
||||
// the error via controller.state if it escalates.
|
||||
// still starts, just without the managed block. We capture the
|
||||
// failure (rather than swallowing via `try?`) so the user gets
|
||||
// a yellow banner explaining the agent won't see project context
|
||||
// for this session, with the underlying error in "Show details".
|
||||
let block = ProjectContextBlock.renderMinimalBlock(
|
||||
projectName: project.name,
|
||||
projectPath: project.path
|
||||
)
|
||||
let ctx = context
|
||||
await Task.detached {
|
||||
try? ProjectContextBlock.writeBlock(
|
||||
block,
|
||||
forProjectAt: project.path,
|
||||
context: ctx
|
||||
)
|
||||
let projectPath = project.path
|
||||
let writeResult: Result<Void, Error> = await Task.detached {
|
||||
do {
|
||||
try ProjectContextBlock.writeBlock(
|
||||
block,
|
||||
forProjectAt: projectPath,
|
||||
context: ctx
|
||||
)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}.value
|
||||
if case .failure(let error) = writeResult {
|
||||
Self.logger.error(
|
||||
"ProjectContextBlock.writeBlock failed for \(projectPath, privacy: .public): \(error.localizedDescription, privacy: .public)"
|
||||
)
|
||||
vm.acpError = "Project context not written — agent will proceed without it."
|
||||
vm.acpErrorHint = "Check that the SSH user can write to \(projectPath)/AGENTS.md."
|
||||
vm.acpErrorDetails = error.localizedDescription
|
||||
}
|
||||
await start(projectPath: project.path, projectName: project.name)
|
||||
}
|
||||
|
||||
@@ -648,9 +670,15 @@ final class ChatController {
|
||||
state = .ready
|
||||
|
||||
// If this was a project-scoped session, record the
|
||||
// attribution so the Mac's per-project Sessions tab picks
|
||||
// it up. Best-effort — ACP session creation already won,
|
||||
// a failed attribution write is cosmetic.
|
||||
// attribution so Dashboard's Sessions tab can render the
|
||||
// project badge for it. Best-effort and intentionally fire-
|
||||
// and-forget — `SessionAttributionService.persist` already
|
||||
// logs SFTP failures via `os.Logger` (see the
|
||||
// `Self.logger.error` in `persist`), and a failed write
|
||||
// here is purely cosmetic: the chat works, only the badge
|
||||
// is missing until the next reconcile. We deliberately
|
||||
// don't surface this to the chat banner because it would
|
||||
// alarm users about a non-issue.
|
||||
if let projectPath {
|
||||
let ctx = context
|
||||
Task.detached {
|
||||
|
||||
Reference in New Issue
Block a user