1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Activity Tracking for Chat View
//!
//! Contains activity stack management, token/metrics updates, and command palette.
use std::time::Duration;
use super::{ActivityItem, ActivityTemp, ChatView, CurrentVerb, TurnMetrics};
// ═══════════════════════════════════════════════════════════════════════════════
// Activity tracking for /exec, /fetch, /agent commands
// ═══════════════════════════════════════════════════════════════════════════════
impl ChatView {
/// Add exec activity to the stack
pub fn add_exec_activity(&mut self, command: &str) {
let activity_id = format!("exec-{}", self.inline_content.len());
self.activity_items
.push(ActivityItem::hot(activity_id, "exec"));
tracing::debug!(command = %command, "Added exec activity");
}
/// Complete exec activity
pub fn complete_exec_activity(&mut self) {
self.transition_activity_to_warm("exec");
}
/// Add fetch activity to the stack
pub fn add_fetch_activity(&mut self, url: &str, method: &str) {
let activity_id = format!("fetch-{}", self.inline_content.len());
self.activity_items
.push(ActivityItem::hot(activity_id, "fetch"));
tracing::debug!(url = %url, method = %method, "Added fetch activity");
}
/// Complete fetch activity
pub fn complete_fetch_activity(&mut self) {
self.transition_activity_to_warm("fetch");
}
/// Add agent activity to the stack
pub fn add_agent_activity(&mut self, goal: &str) {
let activity_id = format!("agent-{}", self.inline_content.len());
self.activity_items
.push(ActivityItem::hot(activity_id, "agent"));
tracing::debug!(goal = %goal, "Added agent activity");
}
/// Complete agent activity
pub fn complete_agent_activity(&mut self) {
self.transition_activity_to_warm("agent");
}
/// Update session token usage
pub fn update_tokens(&mut self, tokens_used: u64, cost: f64) {
self.session_context.tokens_used = tokens_used;
self.session_context.total_cost = cost;
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Real-time Streaming Updates
// ═══════════════════════════════════════════════════════════════════════════════
impl ChatView {
/// Update current verb during execution (for Mission Control display)
pub fn set_current_verb(&mut self, verb: CurrentVerb) {
self.current_verb = verb;
}
/// Update turn metrics during streaming (real-time token counts)
pub fn update_turn_metrics(&mut self, input_tokens: u64, output_tokens: u64, cost_usd: f64) {
// Compute deltas before updating turn metrics
let input_delta = input_tokens.saturating_sub(self.turn_metrics.input_tokens);
let output_delta = output_tokens.saturating_sub(self.turn_metrics.output_tokens);
let cost_delta = cost_usd.max(0.0) - self.turn_metrics.cost_usd.max(0.0);
// Update turn metrics
self.turn_metrics.input_tokens = input_tokens;
self.turn_metrics.output_tokens = output_tokens;
self.turn_metrics.cost_usd = cost_usd;
// Update session metrics with deltas
self.session_metrics.input_tokens += input_delta;
self.session_metrics.output_tokens += output_delta;
self.session_metrics.cost_usd += cost_delta;
}
/// Increment turn metrics during streaming (delta updates)
pub fn increment_output_tokens(&mut self, delta_tokens: u64) {
self.turn_metrics.output_tokens += delta_tokens;
self.session_metrics.output_tokens += delta_tokens;
}
/// Reset turn metrics for a new turn
pub fn reset_turn_metrics(&mut self) {
self.turn_metrics = TurnMetrics::default();
self.current_verb = CurrentVerb::None;
}
/// Complete a turn (session metrics already updated via update_turn_metrics)
pub fn complete_turn(&mut self) {
// Session metrics are already updated incrementally via update_turn_metrics()
// Just reset turn metrics for the next turn
self.reset_turn_metrics();
// Clear inline boxes and activities on turn completion
self.inline_content.clear();
self.activity_items.clear();
}
/// Toggle command palette visibility
pub fn toggle_command_palette(&mut self) {
self.command_palette.toggle();
}
/// Toggle Provider Modal visibility (⌘P)
///
/// Returns true if the modal was opened (verification needed).
pub fn toggle_provider_modal(&mut self) -> bool {
let was_visible = self.provider_modal.visible;
self.provider_modal.visible = !self.provider_modal.visible;
// Return true if we just opened the modal (trigger verification)
!was_visible && self.provider_modal.visible
}
/// Transition activity from hot to warm
/// Use .rev() to find the LAST hot activity (most recent)
/// Before: Only first concurrent op transitioned, others stuck forever
pub(super) fn transition_activity_to_warm(&mut self, verb: &str) {
if let Some(item) = self
.activity_items
.iter_mut()
.rev() // Search from newest to oldest
.find(|i| i.verb == verb && i.temp == ActivityTemp::Hot)
{
item.temp = ActivityTemp::Warm;
item.duration = item.elapsed();
}
}
/// Clear completed (warm) activities older than duration
/// Also remove stuck Hot items that have been running too long
pub fn clear_old_activities(&mut self, max_age_secs: u64) {
let max_duration = Duration::from_secs(max_age_secs);
self.activity_items.retain(|item| {
match item.temp {
ActivityTemp::Warm => {
// Remove warm items older than max_age
item.duration.map(|d| d < max_duration).unwrap_or(true)
}
ActivityTemp::Hot => {
// Remove stuck hot items (running > max_age = likely hung)
item.started
.map(|s| s.elapsed() < max_duration)
.unwrap_or(true)
}
ActivityTemp::Queued => true, // Keep queued items
}
});
}
}