diff --git a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
index d7548daa0..5eb790ca0 100644
--- a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
+++ b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
@@ -3132,7 +3132,6 @@ struct OperatorLanePopoverView: View {
return [
contextMetric(lifecycleMetrics),
- toolsMetric(lifecycleMetrics),
trackerMetric(lifecycleMetrics),
protocolMetric(lifecycleMetrics),
].compactMap { $0 }
@@ -3166,31 +3165,6 @@ struct OperatorLanePopoverView: View {
)
}
- private func toolsMetric(_ metrics: OperatorLifecycleMetrics) -> OperatorTotalMetric? {
- var items = [OperatorLaneReadoutItem]()
- if metrics.toolCallCount > 0 {
- items.append(OperatorLaneReadoutItem(label: "tools", value: formatCompactCount(metrics.toolCallCount)))
- }
-
- if let largestOutput = metrics.largestToolOutputBytes, largestOutput > 0 {
- items.append(
- OperatorLaneReadoutItem(
- label: "max output",
- value: formatLargestOutput(bytes: largestOutput)
- )
- )
- }
-
- guard items.isEmpty == false else {
- return nil
- }
-
- return OperatorTotalMetric(
- title: "Tools",
- items: items
- )
- }
-
private func trackerMetric(_ metrics: OperatorLifecycleMetrics) -> OperatorTotalMetric? {
guard let bucket = lifecycleBucket(named: "Tracker", in: metrics.buckets) else {
return nil
diff --git a/apps/decodex-app/Sources/DecodexApp/AccountStore.swift b/apps/decodex-app/Sources/DecodexApp/AccountStore.swift
index d4d2005e9..587f49244 100644
--- a/apps/decodex-app/Sources/DecodexApp/AccountStore.swift
+++ b/apps/decodex-app/Sources/DecodexApp/AccountStore.swift
@@ -231,12 +231,12 @@ final class AccountStore: ObservableObject {
activeRunsComplete: payload.activeRunsComplete ?? true,
emittedAt: payload.emittedAt ?? Date()
)
- liveRunActivity = activity
if let operatorSnapshot {
self.operatorSnapshot = activity.merging(into: operatorSnapshot)
} else {
operatorSnapshot = OperatorSnapshotResponse.activeRunsOnly(activeRuns)
}
+ liveRunActivity = activity.shouldPersistAsSnapshotOverlay ? activity : nil
operatorSnapshotUpdatedAt = activity.emittedAt
default:
break
diff --git a/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift b/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift
index 3959159e1..b4ba73a81 100644
--- a/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift
+++ b/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift
@@ -797,6 +797,10 @@ struct OperatorRunActivitySnapshot: Sendable {
let activeRunsComplete: Bool
let emittedAt: Date
+ var shouldPersistAsSnapshotOverlay: Bool {
+ activeRuns.isEmpty == false || activeRunsComplete == false
+ }
+
func merging(into snapshot: OperatorSnapshotResponse) -> OperatorSnapshotResponse {
snapshot.mergingRunActivity(activeRuns, activeRunsComplete: activeRunsComplete)
}
diff --git a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift
index f3549821d..f9cb1635f 100644
--- a/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift
+++ b/apps/decodex-app/Tests/DecodexAppTests/AccountModelTests.swift
@@ -866,6 +866,71 @@ final class AccountModelTests: XCTestCase {
XCTAssertTrue(store.operatorSnapshot?.activeRuns(for: account).isEmpty ?? false)
}
+ @MainActor
+ func testCompleteEmptyRunActivityDoesNotKeepClearingNewSnapshots() throws {
+ let account = makeAccount(
+ status: "available",
+ email: "copy@example.com",
+ accountFingerprint: "...123456"
+ )
+ let store = AccountStore()
+
+ try store.applyOperatorDashboardEvent(dashboardEvent(
+ type: "snapshot",
+ payload: """
+ {
+ "snapshotPublishedAtUnixEpoch": 20,
+ "snapshot": {
+ "active_runs": [
+ {
+ "run_id": "run-live",
+ "issue_identifier": "XY-672",
+ "account": {
+ "email": "copy@example.com",
+ "account_fingerprint": "...123456"
+ }
+ }
+ ]
+ }
+ }
+ """
+ ))
+ try store.applyOperatorDashboardEvent(dashboardEvent(
+ type: "runActivity",
+ payload: """
+ {
+ "emittedAtUnixEpoch": 30,
+ "activeRunsComplete": true,
+ "activeRuns": []
+ }
+ """
+ ))
+ XCTAssertTrue(store.operatorSnapshot?.activeRuns(for: account).isEmpty ?? false)
+
+ try store.applyOperatorDashboardEvent(dashboardEvent(
+ type: "snapshot",
+ payload: """
+ {
+ "snapshotPublishedAtUnixEpoch": 40,
+ "snapshot": {
+ "active_runs": [
+ {
+ "run_id": "run-returned",
+ "issue_identifier": "XY-934",
+ "account": {
+ "email": "copy@example.com",
+ "account_fingerprint": "...123456"
+ }
+ }
+ ]
+ }
+ }
+ """
+ ))
+
+ XCTAssertEqual(store.operatorSnapshot?.activeRuns(for: account).map(\.runID), ["run-returned"])
+ }
+
func testOperatorSnapshotWarningSummaryUsesRawWarningToken() throws {
let payload = """
{
diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html
index cf56ac393..f3c864d8c 100644
--- a/apps/decodex/src/orchestrator/operator_dashboard.html
+++ b/apps/decodex/src/orchestrator/operator_dashboard.html
@@ -8606,23 +8606,6 @@
Run History
rows.push({ label: "Context", segments: contextSegments });
}
- const toolCalls = lifecycleNumber(lifecycle?.tool_call_count);
- const largestOutput = lifecycle?.largest_tool_output_bytes;
- const toolSegments = [];
- if (toolCalls > 0) {
- appendLifecycleMetricSegment(toolSegments, formatCompactCount(toolCalls), "tools");
- }
- if (largestOutput != null) {
- appendLifecycleMetricSegment(
- toolSegments,
- formatLargestOutputValue(largestOutput),
- "max output",
- );
- }
- if (toolSegments.length) {
- rows.push({ label: "Tools", segments: toolSegments });
- }
-
const tracker = lifecycleBucket(lifecycle, "Tracker");
if (tracker) {
const trackerSegments = [];
@@ -11321,17 +11304,37 @@ ${escapeHtml(worktree.branch_name)}
};
}
- function snapshotWithLiveRunActivity(snapshot) {
- if (!snapshot || !dashboardSocketIsOpen()) {
- return snapshot;
+ function dashboardLiveRunActivityHasOverlay({ includeCompletedEmpty = false } = {}) {
+ if (!dashboardLiveRunActivitySeen) {
+ return false;
}
+ if (dashboardLiveActiveRuns.length || !dashboardLiveActiveRunsComplete) {
+ return true;
+ }
+
+ return includeCompletedEmpty;
+ }
+ function clearDashboardLiveRunActivityOverlayIfCompleteEmpty() {
if (
- !dashboardLiveRunActivitySeen &&
- !dashboardLiveActiveRuns.length &&
- !dashboardLiveAccounts &&
- !dashboardLiveAccountControl
+ dashboardLiveRunActivitySeen &&
+ dashboardLiveActiveRunsComplete &&
+ !dashboardLiveActiveRuns.length
) {
+ dashboardLiveRunActivitySeen = false;
+ dashboardLiveActiveRuns = [];
+ dashboardLiveActiveRunsComplete = true;
+ dashboardLiveAccounts = null;
+ dashboardLiveAccountControl = null;
+ }
+ }
+
+ function snapshotWithLiveRunActivity(snapshot, options = {}) {
+ if (!snapshot || !dashboardSocketIsOpen()) {
+ return snapshot;
+ }
+
+ if (!dashboardLiveRunActivityHasOverlay(options)) {
return snapshot;
}
@@ -11373,13 +11376,16 @@ ${escapeHtml(worktree.branch_name)}
error: false,
lastEventAt: new Date().toISOString(),
});
+ clearDashboardLiveRunActivityOverlayIfCompleteEmpty();
return;
}
lastDashboardRender = {
...lastDashboardRender,
- snapshot: snapshotWithLiveRunActivity(lastDashboardRender.snapshot),
+ snapshot: snapshotWithLiveRunActivity(lastDashboardRender.snapshot, {
+ includeCompletedEmpty: true,
+ }),
};
updateDashboardStreamState(
{
@@ -11390,6 +11396,7 @@ ${escapeHtml(worktree.branch_name)}
false,
);
renderDashboardState(lastDashboardRender);
+ clearDashboardLiveRunActivityOverlayIfCompleteEmpty();
}
function applyDashboardSnapshotEvent(payload) {
diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs
index 8ad4f4f68..cecf0b7f1 100644
--- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs
+++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs
@@ -442,7 +442,6 @@ fn assert_child_lifecycle_contract(response: &str) {
"const header = [\"Stage\", \"attempts\", \"inference\", \"input\", \"output\", \"tools\", \"max output\"];",
"const alignRight = new Set([1, 2, 3, 4, 5, 6]);",
"width: fit-content;\n\t\t\t\tmax-width: 100%;",
- "appendLifecycleMetricSegment(\n\t\t\t\t\t\t\ttoolSegments,\n\t\t\t\t\t\t\tformatLargestOutputValue(largestOutput),\n\t\t\t\t\t\t\t\"max output\",",
"\"tools\"",
"output bytes",
"field(\"Large outputs\", childAgentLargeOutputSummary(childAgentActivity(run)))",
@@ -470,6 +469,8 @@ fn assert_child_lifecycle_contract(response: &str) {
"const titleAttribute = segment.help ? ` title=\"${escapeHtml(segment.help)}\"` : \"\";",
"largestOutputHelp(largestOutput, lifecycle?.largest_tool_output_tool)",
"formatLargestOutputValue(largestOutput, lifecycle?.largest_tool_output_tool)",
+ "const toolSegments = [];",
+ "rows.push({ label: \"Tools\", segments: toolSegments });",
],
);
}
@@ -1834,7 +1835,12 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() {
assert!(response.contains("let dashboardLiveActiveRunsComplete = true;"));
assert!(response.contains("let dashboardLiveAccounts = null;"));
assert!(response.contains("let dashboardLiveAccountControl = null;"));
- assert!(response.contains("function snapshotWithLiveRunActivity(snapshot)"));
+ assert!(response
+ .contains("function dashboardLiveRunActivityHasOverlay({ includeCompletedEmpty = false } = {})"));
+ assert!(response.contains("function clearDashboardLiveRunActivityOverlayIfCompleteEmpty()"));
+ assert!(response.contains("return includeCompletedEmpty;"));
+ assert!(response.contains("function snapshotWithLiveRunActivity(snapshot, options = {})"));
+ assert!(response.contains("if (!dashboardLiveRunActivityHasOverlay(options))"));
assert!(response.contains("\"issue_identifier\""));
assert!(response.contains("\"title\""));
assert!(!response.contains("field(\"Author\","));
@@ -1858,6 +1864,10 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() {
assert!(response.contains("dashboardLiveRunActivitySeen = true;"));
assert!(response.contains("dashboardLiveActiveRunsComplete ="));
assert!(response.contains("snapshot: snapshotWithLiveRunActivity(payload.snapshot),"));
+ assert!(response.contains(
+ "snapshot: snapshotWithLiveRunActivity(lastDashboardRender.snapshot, {\n\t\t\t\t\t\tincludeCompletedEmpty: true,\n\t\t\t\t\t}),"
+ ));
+ assert!(response.contains("clearDashboardLiveRunActivityOverlayIfCompleteEmpty();"));
assert!(response.contains("account_control: accountControl,"));
assert!(response.contains("accounts,"));
assert!(response.contains("active_runs: mergedActiveRuns,"));