-
Notifications
You must be signed in to change notification settings - Fork 3
macOS Live tab: provider overlays, time-range switcher, range totals #485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
779a58c
2ece648
d943f02
80c42ae
d8a2a77
b94c7a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,15 +77,72 @@ actor BurnLedger { | |
| return Summary(cost: cost, tokens: tokens) | ||
| } | ||
|
|
||
| // MARK: - Ingest | ||
|
|
||
| /// Runs one incremental `burn ingest` sweep so the ledger reflects freshly | ||
| /// written turns. `burn summary` is read-only (it no longer ingests), and the | ||
| /// background `ingest --watch` doesn't keep up, so the live monitor calls this | ||
| /// each poll to keep the numbers moving. A warm incremental sweep is fast | ||
| /// (only new turns). No-op / `nil` when burn is unavailable. | ||
| func ingest() { | ||
| _ = runBurn(["ingest", "--quiet"]) | ||
| /// One bucket of a `burn summary --bucket` time-series. | ||
| struct TimeseriesPoint { | ||
| let date: Date | ||
| let tokens: Int | ||
| let cost: Double | ||
| } | ||
|
|
||
| /// Per-bucket cost/token totals for `provider` over `[since, now]`, bucketed | ||
| /// by `bucket` (a burn duration like "30s"/"5m"/"1h"). Returns `nil` when | ||
| /// burn is unavailable or the query/parse fails. Runs `burn summary --bucket` | ||
| /// (read-only), so it's cheap to refresh. | ||
| func timeseries(provider: String, since: Date, bucket: String) async -> [TimeseriesPoint]? { | ||
| let iso = ISO8601DateFormatter().string(from: since) | ||
| let args = ["summary", "--provider", provider, "--since", iso, "--bucket", bucket, "--json"] | ||
| guard let output = runBurn(args), | ||
| let data = output.data(using: .utf8), | ||
| let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], | ||
| let buckets = json["buckets"] as? [[String: Any]] | ||
| else { return nil } | ||
|
|
||
| let parser = ISO8601DateFormatter() | ||
| parser.formatOptions = [.withInternetDateTime, .withFractionalSeconds] | ||
| let plain = ISO8601DateFormatter() | ||
|
|
||
| return buckets.compactMap { entry -> TimeseriesPoint? in | ||
| guard let start = entry["start"] as? String, | ||
| let date = parser.date(from: start) ?? plain.date(from: start) | ||
| else { return nil } | ||
| let tokens = (entry["totalTokens"] as? NSNumber)?.intValue ?? 0 | ||
| let cost = ((entry["totalCost"] as? [String: Any])?["total"] as? NSNumber)?.doubleValue ?? 0 | ||
| return TimeseriesPoint(date: date, tokens: tokens, cost: cost) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Long-lived ingest watch | ||
|
|
||
| /// The running `burn ingest --watch` process, if any. `burn summary` is | ||
| /// read-only (~10ms) but a one-shot `burn ingest` sweep is multi-second on a | ||
| /// large ledger — far too slow to run per poll. Instead this long-lived watch | ||
| /// keeps the ledger fresh incrementally (FS-event driven, ~1s poll), so the | ||
| /// live view's summary polls stay fast. | ||
| private var watchProcess: Process? | ||
|
|
||
| /// Starts a background `burn ingest --watch` if one isn't already running. | ||
| /// Only runs with the bundled native helper (a login-shell child can't be | ||
| /// cleanly managed); the live chart still polls either way. | ||
| func startIngestWatch() { | ||
| guard watchProcess == nil else { return } | ||
| guard case .bundled(let url) = resolveTool() else { return } | ||
| let process = Process() | ||
| process.executableURL = url | ||
| process.arguments = ["ingest", "--watch", "--quiet"] | ||
| process.standardOutput = Pipe() | ||
| process.standardError = Pipe() | ||
|
Comment on lines
+132
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In a ledger state that causes repeated ingest errors, Useful? React with 👍 / 👎. |
||
| do { | ||
| try process.run() | ||
| watchProcess = process | ||
| } catch { | ||
| watchProcess = nil | ||
| } | ||
| } | ||
|
|
||
| /// Terminates the background watch process, if running. | ||
| func stopIngestWatch() { | ||
| watchProcess?.terminate() | ||
| watchProcess = nil | ||
| } | ||
|
|
||
| // MARK: - Resolution & invocation | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the app is run via
swift runor built without bundling the helper but aburnexists on PATH,resolveTool()returns.pathhere and the new watch is skipped. Since this commit also removed the per-pollingest()call and the Live tab only calls read-onlysummary --bucket, PATH-only builds no longer freshen the ledger, so the live chart stays stale unless the user separately starts an ingest watcher.Useful? React with 👍 / 👎.