mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
perf(ios): wire v2.7 perf parity — instrument iOS-only VMs + surface hydration banner + opt-in toggle
Most of the v2.7 perf work was already covered on iOS via shared code in ScarfCore — `RichChatViewModel.loadSessionHistory` (and its skeleton-then-hydrate path), `hydrateAssistantToolCalls`, `fetchSkeletonMessages`, `fetchRecentToolCallSkeleton`, `ModelPreflight.detectMismatch`, and the `RemoteSQLiteBackend` cancellation handler all flow through to the ScarfGo chat unchanged. `CitadelServerTransport.streamScript` already honors `Task.isCancelled` correctly via `withThrowingTaskGroup` + `Task.checkCancellation()`, so the SSH-cancellation-on-nav-away chain works on iOS without the Mac-side `SSHScriptRunner` fix. Three iOS-specific gaps closed: * IOSCronViewModel.load + IOSMemoryViewModel.load wrapped in `ScarfMon.measureAsync(.diskIO, "ios.cron.load")` / `"ios.memory.load"` — parity with the Mac `cron.load` / `memory.load` events. `ios.memory.load.bytes` records the payload size for the loaded file. * iOS Settings → "Chat (Scarf)" section gains a toggle bound to `RichChatViewModel.loadHistoricalToolResultsKey` so iOS users can opt into Phase 2b bulk tool-result hydration, same as the Mac DisplayTab. The shared key means the gate inside `startToolHydration` reads the right value automatically — no extra plumbing needed. * iOS ChatView surfaces `isHydratingTools` as a "Loading tool details…" connection banner (mirrors the Mac toolbar pill added in v2.7 perf work). Sits between the existing "Thinking…" banner and the empty-view fallback so chat status is always honest about what the agent and Scarf are doing. Both Mac and iOS targets build clean; all 321 ScarfCore tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,17 +29,24 @@ public final class IOSCronViewModel {
|
||||
let ctx = context
|
||||
let path = ctx.paths.cronJobsJSON
|
||||
|
||||
let result: Result<CronJobsFile, Error> = await Task.detached {
|
||||
do {
|
||||
guard let data = ctx.readData(path) else {
|
||||
throw LoadError.missingFile(path: path)
|
||||
// v2.7 — instrumented for parity with Mac `cron.load`. iOS
|
||||
// Cron load is a single SFTP read of jobs.json so should be
|
||||
// snappy on most remotes; this measure point makes the cost
|
||||
// visible in ScarfMon traces alongside the rest of the iOS
|
||||
// load paths.
|
||||
let result: Result<CronJobsFile, Error> = await ScarfMon.measureAsync(.diskIO, "ios.cron.load") {
|
||||
await Task.detached {
|
||||
do {
|
||||
guard let data = ctx.readData(path) else {
|
||||
throw LoadError.missingFile(path: path)
|
||||
}
|
||||
let decoded = try JSONDecoder().decode(CronJobsFile.self, from: data)
|
||||
return .success(decoded)
|
||||
} catch {
|
||||
return Result<CronJobsFile, Error>.failure(error)
|
||||
}
|
||||
let decoded = try JSONDecoder().decode(CronJobsFile.self, from: data)
|
||||
return .success(decoded)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}.value
|
||||
}.value
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let file):
|
||||
|
||||
@@ -96,15 +96,24 @@ public final class IOSMemoryViewModel {
|
||||
// Run the file read on a detached task — `readTextThrowing`
|
||||
// blocks on transport I/O, and we don't want the MainActor
|
||||
// hanging during a remote SFTP fetch.
|
||||
// v2.7 — instrumented for parity with Mac `memory.load`.
|
||||
// iOS path is one SFTP read per Memory tab open (per kind:
|
||||
// memory / user / soul); the bytes counter shows payload
|
||||
// size alongside latency.
|
||||
let ctx = context
|
||||
let path = kind.path(on: context)
|
||||
let result: Result<String?, Error> = await Task.detached {
|
||||
do {
|
||||
return .success(try ctx.readTextThrowing(path))
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}.value
|
||||
let result: Result<String?, Error> = await ScarfMon.measureAsync(.diskIO, "ios.memory.load") {
|
||||
await Task.detached {
|
||||
do {
|
||||
return Result<String?, Error>.success(try ctx.readTextThrowing(path))
|
||||
} catch {
|
||||
return Result<String?, Error>.failure(error)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
if case .success(.some(let loaded)) = result {
|
||||
ScarfMon.event(.diskIO, "ios.memory.load.bytes", count: 0, bytes: loaded.utf8.count)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(.some(let loaded)):
|
||||
|
||||
@@ -412,6 +412,16 @@ struct ChatView: View {
|
||||
tint: ScarfColor.info,
|
||||
showSpinner: true
|
||||
)
|
||||
} else if controller.vm.isHydratingTools {
|
||||
// v2.7 — Phase 2 tool-call hydration is in flight.
|
||||
// Bare conversation skeleton is already on screen;
|
||||
// this banner tells the user the tool cards are
|
||||
// about to fill in.
|
||||
connectionBannerStrip(
|
||||
text: "Loading tool details…",
|
||||
tint: ScarfColor.info,
|
||||
showSpinner: true
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ struct SettingsView: View {
|
||||
@State private var vm: IOSSettingsViewModel
|
||||
@State private var showRawYAML = false
|
||||
@State private var editingSpec: SettingSpec?
|
||||
/// v2.7 — Scarf-local opt-in to bulk-fetch tool result CONTENT
|
||||
/// when resuming past chats. Default off; the shared
|
||||
/// `RichChatViewModel` reads this same UserDefaults key on
|
||||
/// every chat resume so iOS gets the same skeleton-then-hydrate
|
||||
/// behavior as Mac.
|
||||
@AppStorage(RichChatViewModel.loadHistoricalToolResultsKey)
|
||||
private var loadHistoricalToolResults: Bool = false
|
||||
|
||||
private static let sharedContextID: ServerID = ServerID(
|
||||
uuidString: "00000000-0000-0000-0000-0000000000A1"
|
||||
@@ -164,6 +171,28 @@ struct SettingsView: View {
|
||||
yesNoRow("Inline diffs", vm.config.display.inlineDiffs)
|
||||
LabeledContent("Personality", value: vm.config.personality)
|
||||
}
|
||||
chatScarfSection
|
||||
}
|
||||
|
||||
/// v2.7 — Scarf-local chat preferences. Mirrors the Mac Settings
|
||||
/// → Display → "Load tool results in past chats" toggle. Lives in
|
||||
/// its own section so it's clear these are app-side settings, not
|
||||
/// Hermes config values.
|
||||
@ViewBuilder
|
||||
private var chatScarfSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: $loadHistoricalToolResults) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Load tool results in past chats")
|
||||
.font(.body)
|
||||
Text("Off (default) keeps past chat resumes fast on slow remotes — tool call cards still render, but the inspector lazy-loads each result when you open it.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat (Scarf)")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user