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
16 changes: 15 additions & 1 deletion Glint/Agent/AgentPaneSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ func agentElapsedLabel(since start: Date, now: Date) -> String {
return "\(total / 3600)h\((total % 3600) / 60)m"
}

/// Busy panes advance against the live clock. Attention states that end or
/// pause the turn keep the duration captured by their last hook instead of
/// making a completed/error/reply-wait row look as if it were still running.
func agentElapsedReferenceDate(status: PaneAgentStatus, updatedAt: Date, now: Date) -> Date {
WorkspaceStore.isBusyStatus(status) ? now : updatedAt
}

// MARK: - Cluster (always-visible glance layer)

/// Up to `cap` attention-sorted status dots — one per live agent pane. This
Expand Down Expand Up @@ -176,7 +183,14 @@ private struct PaneSummaryRow: View {
)

TimelineView(.periodic(from: .now, by: 1)) { ctx in
Text(agentElapsedLabel(since: info.since, now: ctx.date))
Text(agentElapsedLabel(
since: info.since,
now: agentElapsedReferenceDate(
status: info.status,
updatedAt: info.updatedAt,
now: ctx.date
)
))
.font(.system(size: 10, weight: .medium, design: .monospaced))
.foregroundStyle(Theme.text4)
.lineLimit(1)
Expand Down
7 changes: 5 additions & 2 deletions Glint/Workspace/WorkspaceStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4289,6 +4289,8 @@ extension WorkspaceStore {
let status: PaneAgentStatus
/// Turn start — drives the "2m" elapsed label (total turn time).
let since: Date
/// Last hook time — freezes the elapsed label once the turn stops.
let updatedAt: Date
var id: PaneID { paneID }
}

Expand All @@ -4304,7 +4306,8 @@ extension WorkspaceStore {
guard let e = paneAgentState[key], e.status != .idle else { continue }
out.append((AgentPaneInfo(paneID: item.pane, number: idx + 1,
label: item.label, kind: e.kind,
status: e.status, since: e.turnStartedAt),
status: e.status, since: e.turnStartedAt,
updatedAt: e.updatedAt),
e.updatedAt))
}
out.sort {
Expand Down Expand Up @@ -4337,7 +4340,7 @@ extension WorkspaceStore {
/// A turn is actively running in these states — used to anchor the turn
/// clock (set on the first non-busy → busy transition, kept through the
/// turn). `.justCompleted`/`.failed`/`.idle` are turn-end / no-turn.
static func isBusyStatus(_ s: PaneAgentStatus) -> Bool {
nonisolated static func isBusyStatus(_ s: PaneAgentStatus) -> Bool {
switch s {
case .thinking, .tool, .compacting, .needsPermission: return true
case .justCompleted, .failed, .needsReply, .idle: return false
Expand Down
18 changes: 18 additions & 0 deletions GlintTests/AgentHookRoutingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ import XCTest
@testable import Glint

final class AgentHookRoutingTests: XCTestCase {
func testCompletedAgentElapsedTimerStopsAtCompletion() {
let completedAt = Date(timeIntervalSince1970: 125)
let muchLater = Date(timeIntervalSince1970: 900)

XCTAssertEqual(
agentElapsedReferenceDate(status: .justCompleted,
updatedAt: completedAt,
now: muchLater),
completedAt
)
XCTAssertEqual(
agentElapsedReferenceDate(status: .thinking,
updatedAt: completedAt,
now: muchLater),
muchLater
)
}

func testAttentionRankDoesNotLetThinkingHideCompletedSibling() {
XCTAssertEqual(
PaneAgentStatus.bestAttentionRank(in: [.thinking, .justCompleted]),
Expand Down