Files
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.

- prioritize selected and visible sessions during bootstrap and defer
  non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
  hidden sidebar work
- prevent stale session and message requests from overwriting newer
  authoritative state
- preserve existing data when authoritative fetches fail instead of treating
  failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
  permissions, folders, tabs, Git state, and pull request data by runtime and
  directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
  persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
  message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
  bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
  runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
  rendering hot paths
- limit virtualization to archive collections where it improves rendering
  without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
  sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
  flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
  reconnect behavior, persistence races, authoritative empty results, and
  subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
  runtime-isolation invariants
2026-07-21 20:52:20 +03:00

139 lines
6.6 KiB
Swift

import SwiftUI
import WidgetKit
// MARK: - Shared model + App Group reader
/// One row of the session overview the app writes to the shared App Group.
/// Mirrors MobileWidgetSession in packages/ui/src/apps/mobileWidgetSnapshot.ts.
struct WidgetSession: Codable, Identifiable, Hashable {
let id: String
let title: String
let unread: Bool
/// Project label for the session's directory. Optional so snapshots written before this
/// field existed still decode.
var project: String?
}
/// The session overview snapshot. Mirrors MobileWidgetSnapshot (same field names) so the
/// JSON the app stores decodes directly.
struct WidgetSnapshot: Codable {
var runtimeKey: String?
let attentionCount: Int
let recentSessions: [WidgetSession]
static let empty = WidgetSnapshot(runtimeKey: nil, attentionCount: 0, recentSessions: [])
}
enum WidgetStore {
static let appGroup = "group.com.openchamber.app"
static let snapshotKey = "widgetSnapshot"
/// Reads the latest snapshot the app persisted. Returns `.empty` when nothing has been
/// written yet (fresh install / app never foregrounded) so widgets render a clean state.
static func load() -> WidgetSnapshot {
guard let defaults = UserDefaults(suiteName: appGroup),
let json = defaults.string(forKey: snapshotKey),
let data = json.data(using: .utf8),
let snapshot = try? JSONDecoder().decode(WidgetSnapshot.self, from: data) else {
return .empty
}
return snapshot
}
}
// MARK: - Deep links (mirror packages/ui/src/apps/deepLinks.ts)
enum WidgetDeepLink {
static func newSession() -> URL { URL(string: "openchamber://new")! }
static func attention() -> URL { URL(string: "openchamber://sessions?filter=attention")! }
static func status() -> URL { URL(string: "openchamber://status")! }
static func settings() -> URL { URL(string: "openchamber://settings")! }
static func changes() -> URL { URL(string: "openchamber://changes")! }
static func files() -> URL { URL(string: "openchamber://view/files")! }
static func instances() -> URL { URL(string: "openchamber://view/instances")! }
static func session(_ id: String) -> URL {
let encoded = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
return URL(string: "openchamber://session/\(encoded)") ?? newSession()
}
}
// MARK: - Timeline provider
struct OverviewEntry: TimelineEntry {
let date: Date
let snapshot: WidgetSnapshot
}
struct OverviewProvider: TimelineProvider {
func placeholder(in context: Context) -> OverviewEntry {
OverviewEntry(date: Date(), snapshot: .empty)
}
func getSnapshot(in context: Context, completion: @escaping (OverviewEntry) -> Void) {
completion(OverviewEntry(date: Date(), snapshot: WidgetStore.load()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<OverviewEntry>) -> Void) {
// The app/NSE reload timelines (WidgetCenter) when the snapshot changes, but with several
// widgets sharing the app's WidgetKit reload budget iOS can refresh them unevenly and
// leave one stale. Ask for a periodic refresh too so every widget independently re-reads
// the shared snapshot and converges to the latest state (budget permitting).
let entry = OverviewEntry(date: Date(), snapshot: WidgetStore.load())
let nextRefresh = Date().addingTimeInterval(10 * 60)
completion(Timeline(entries: [entry], policy: .after(nextRefresh)))
}
}
// MARK: - Logo (full OpenChamber mark drawn from the SVG)
/// The OpenChamber logo, drawn to match packages/web/public/logo-dark-512x512.svg: an
/// isometric cube with translucent face fills, stroked edges, and the OpenCode mark on the
/// top face. Faces use low-opacity `.primary` so the system tint on the Lock Screen / Control
/// Center reads as a translucent fill (no colour) rather than a flat wireframe. Coordinates are
/// the SVG inner group (range x:-41.568…41.568, y:-48…48).
struct CubeLogoView: View {
var body: some View {
Canvas { context, size in
let halfW: CGFloat = 41.568
let halfH: CGFloat = 48
let scale = min(size.width / (halfW * 2), size.height / (halfH * 2))
let cx = size.width / 2
let cy = size.height / 2
let lineWidth = max(1.5, 3 * scale)
// Cube coordinate → canvas point.
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { CGPoint(x: cx + x * scale, y: cy + y * scale) }
// OpenCode-mark local coordinate → canvas point (SVG: matrix(0.866,0.5,-0.866,0.5,0,-24) · scale(0.75)).
func m(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
let s: CGFloat = 0.75
let mx = 0.866 * s * x - 0.866 * s * y
let my = 0.5 * s * x + 0.5 * s * y - 24
return p(mx, my)
}
var left = Path()
left.move(to: p(0, 0)); left.addLine(to: p(-halfW, -24)); left.addLine(to: p(-halfW, 24)); left.addLine(to: p(0, 48)); left.closeSubpath()
var right = Path()
right.move(to: p(0, 0)); right.addLine(to: p(halfW, -24)); right.addLine(to: p(halfW, 24)); right.addLine(to: p(0, 48)); right.closeSubpath()
var top = Path()
top.move(to: p(0, -48)); top.addLine(to: p(-halfW, -24)); top.addLine(to: p(0, 0)); top.addLine(to: p(halfW, -24)); top.closeSubpath()
context.fill(left, with: .color(.primary.opacity(0.2)))
context.fill(right, with: .color(.primary.opacity(0.35)))
context.stroke(left, with: .color(.primary), style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))
context.stroke(right, with: .color(.primary), style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))
context.stroke(top, with: .color(.primary), style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))
// OpenCode mark: square ring (even-odd) + a partial inner fill.
var ring = Path()
ring.move(to: m(-16, -20)); ring.addLine(to: m(16, -20)); ring.addLine(to: m(16, 20)); ring.addLine(to: m(-16, 20)); ring.closeSubpath()
ring.move(to: m(-8, -12)); ring.addLine(to: m(-8, 12)); ring.addLine(to: m(8, 12)); ring.addLine(to: m(8, -12)); ring.closeSubpath()
context.fill(ring, with: .color(.primary), style: FillStyle(eoFill: true))
var inner = Path()
inner.move(to: m(-8, -4)); inner.addLine(to: m(8, -4)); inner.addLine(to: m(8, 12)); inner.addLine(to: m(-8, 12)); inner.closeSubpath()
context.fill(inner, with: .color(.primary.opacity(0.4)))
}
}
}