Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,27 @@ public struct CustomMetric: Codable, Sendable, Equatable {
public var title: String
public var formattedValue: String
public var normalizedValue: Double?
public var detail: String?
public var state: CustomMetricState?

public init(title: String, formattedValue: String, normalizedValue: Double? = nil) {
public init(
title: String,
formattedValue: String,
normalizedValue: Double? = nil,
detail: String? = nil,
state: CustomMetricState? = nil
) {
self.title = title
self.formattedValue = formattedValue
self.normalizedValue = normalizedValue
self.detail = detail
self.state = state
}
}

public enum CustomMetricState: String, Codable, Sendable, Equatable {
case active
case waiting
case completed
case error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
CustomMetricView.swift
UserInterface

Created on 2026/07/15.
Copyright 2026 Kyome22 (Takuto Nakamura)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import DataSource
import SwiftUI

struct CustomMetricView: View {
var metric: CustomMetric

var body: some View {
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 5) {
if let state = metric.state {
Circle()
.fill(state.color)
.frame(width: 7, height: 7)
.accessibilityHidden(true)
}
Text(verbatim: metric.title)
Spacer(minLength: 4)
Text(verbatim: metric.formattedValue)
.foregroundStyle(.secondary)
}
.font(.caption)
if let detail = metric.detail {
Text(verbatim: detail)
.font(.caption2)
.foregroundStyle(.tertiary)
}
if let normalizedValue = metric.normalizedValue {
BarGraphView(value: max(0, min(1, normalizedValue)) * 100)
}
}
}
}

private extension CustomMetricState {
var color: Color {
switch self {
case .active: .accentColor
case .waiting: .orange
case .completed: .green
case .error: .red
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ struct CustomMetricsCardView: View {
Text(verbatim: snapshot.title)
Group {
ForEach(snapshot.metrics.enumerated(), id: \.offset) { _, metric in
Text(verbatim: "\(metric.title): \(metric.formattedValue)")
.font(.caption)
if let normalizedValue = metric.normalizedValue {
BarGraphView(value: max(0, min(1, normalizedValue)) * 100)
}
CustomMetricView(metric: metric)
}
Text("lastUpdated:\(lastUpdatedDetail)", bundle: .module)
.font(.caption)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,38 @@ struct CustomMetricsSnapshotTests {
#expect(snapshot == expected)
}

@Test
func decode_snapshot_with_metric_detail_and_state() throws {
let json = """
{
"title": "Sessions",
"metrics": [
{
"title": "Build dashboard",
"formattedValue": "Waiting",
"detail": "/Users/example/project",
"state": "waiting"
}
],
"lastUpdatedDate": "2026-06-05T04:50:40Z"
}
""".data(using: .utf8)!
#expect(
try decoder.decode(CustomMetricsSnapshot.self, from: json) == CustomMetricsSnapshot(
title: "Sessions",
metrics: [
CustomMetric(
title: "Build dashboard",
formattedValue: "Waiting",
detail: "/Users/example/project",
state: .waiting
),
],
lastUpdatedDate: try #require(ISO8601DateFormatter().date(from: "2026-06-05T04:50:40Z"))
)
)
}

@Test
func decode_throws_when_title_missing() {
let json = """
Expand Down
11 changes: 9 additions & 2 deletions docs/CustomMetricsSchema.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ A valid file might look like this:
"symbol": "staroflife",
"metricsBarValue": "5.4%",
"metrics": [
{ "title": "Model", "formattedValue": "Opus 4.7" },
{
"title": "Model",
"formattedValue": "Opus 4.7",
"detail": "~/Projects/example",
"state": "active"
},
{ "title": "Context", "formattedValue": "5.4%", "normalizedValue": 0.054 },
{ "title": "5h", "formattedValue": "16.4%", "normalizedValue": 0.164 },
{ "title": "7d", "formattedValue": "1.0%", "normalizedValue": 0.01 }
Expand All @@ -29,7 +34,7 @@ A valid file might look like this:
}
```

The values above are illustrative`title`, `symbol`, and the metric labels are all your choice; pick whatever makes sense for what you're tracking. The `Model` row omits `normalizedValue`, so RunCat renders just the text with no bar; the other three rows include it, so a bar is drawn under the text.
The values above are illustrative. `title`, `symbol`, and the metric labels are all your choice; pick whatever makes sense for what you're tracking. The `Model` row includes secondary detail and an active-state indicator but omits `normalizedValue`, so it has no bar. The other three rows include `normalizedValue`, so a bar is drawn under their text.

### Top level

Expand All @@ -48,6 +53,8 @@ The values above are illustrative — `title`, `symbol`, and the metric labels a
| `title` | string | yes | Row label. The row is rendered as `title: formattedValue`. |
| `formattedValue` | string | yes | The completed display string. Include any units, currency symbols, or suffixes (e.g. `"5.4%"`, `"$3.21"`, `"42 days"`). |
| `normalizedValue` | number | no | A value between 0 and 1. When present, a horizontal progress bar is drawn under the row. When omitted, only the `title: formattedValue` text is shown. |
| `detail` | string | no | Secondary text shown below the row. |
| `state` | string | no | Semantic status: `active`, `waiting`, `completed`, or `error`. Shown as a colored dot. |

## Display rules

Expand Down