Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 32 additions & 15 deletions dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ function pollFocusUsage() {
if (!fa) return;
var url = "/v1/agents/" + encodeURIComponent(fa.workspace) + "/" + encodeURIComponent(fa.agent) + "/focus?lines=1&usage=1";
fetch(url).then(function(res) { return res.json(); }).then(function(json) {
if (!appState.focusAgent || appState.focusAgent.agent !== fa.agent) return;
if (!appState.focusAgent || appState.focusAgent.workspace !== fa.workspace || appState.focusAgent.agent !== fa.agent) return;
if (!json.data || !json.data.usage) return;
// Update just the usage stats section
var u = json.data.usage;
Expand All @@ -746,6 +746,12 @@ function pollFocusUsage() {
statsHtml += statRow("output tokens", formatTokens(u.output_tokens || 0));
statsHtml += statRow("cache read", formatTokens(u.cache_read || 0), "green");
statsHtml += statRow("cache write", formatTokens(u.cache_write || 0));
var ctxPct = json.data.context_pct;
if (ctxPct != null) {
var ctxColor = ctxPct <= 70 ? "green" : (ctxPct <= 90 ? "amber" : "red");
statsHtml += statRow("context", ctxPct + "%", ctxColor);
statsHtml += '<div class="focus-ctx-bar"><div class="focus-ctx-fill" style="width:' + ctxPct + '%;background:var(--' + (ctxColor === "green" ? "working" : ctxColor === "amber" ? "auth-fail" : "blocked") + ')"></div></div>';
}
$focusStats.innerHTML = statsHtml;
}).catch(function() { /* silently ignore usage poll errors */ });
}
Expand Down Expand Up @@ -788,23 +794,34 @@ function renderFocusView(data) {
if (wasAtBottom) termEl.scrollTop = termEl.scrollHeight;
}

// Usage stats
var u = data.usage || {};
var statsHtml = "";
statsHtml += statRow("input tokens", formatTokens(u.input_tokens || 0));
statsHtml += statRow("output tokens", formatTokens(u.output_tokens || 0));
statsHtml += statRow("cache read", formatTokens(u.cache_read || 0), "green");
statsHtml += statRow("cache write", formatTokens(u.cache_write || 0));
// Context bar
var ctxPct = data.context_pct;
if (ctxPct != null) {
// Usage stats — only update if this response includes usage data.
// The separate pollFocusUsage() handles the slow usage poll; don't
// overwrite it with zeros from the fast terminal poll.
if (data.usage && (data.usage.input_tokens || data.usage.output_tokens || data.usage.cache_read || data.usage.cache_write)) {
var u = data.usage;
var statsHtml = "";
statsHtml += statRow("input tokens", formatTokens(u.input_tokens || 0));
statsHtml += statRow("output tokens", formatTokens(u.output_tokens || 0));
statsHtml += statRow("cache read", formatTokens(u.cache_read || 0), "green");
statsHtml += statRow("cache write", formatTokens(u.cache_write || 0));
// Context bar
var ctxPct = data.context_pct;
if (ctxPct != null) {
var ctxColor = ctxPct <= 70 ? "green" : (ctxPct <= 90 ? "amber" : "red");
statsHtml += statRow("context", ctxPct + "%", ctxColor);
statsHtml += '<div class="focus-ctx-bar"><div class="focus-ctx-fill" style="width:' + ctxPct + '%;background:var(--' + (ctxColor === "green" ? "working" : ctxColor === "amber" ? "auth-fail" : "blocked") + ')"></div></div>';
} else {
statsHtml += statRow("context", "\u2014");
}
$focusStats.innerHTML = statsHtml;
} else if (data.context_pct != null && $focusStats.innerHTML === "") {
// At least show context bar even without full usage data
var ctxPct = data.context_pct;
var ctxColor = ctxPct <= 70 ? "green" : (ctxPct <= 90 ? "amber" : "red");
statsHtml += statRow("context", ctxPct + "%", ctxColor);
var statsHtml = statRow("context", ctxPct + "%", ctxColor);
statsHtml += '<div class="focus-ctx-bar"><div class="focus-ctx-fill" style="width:' + ctxPct + '%;background:var(--' + (ctxColor === "green" ? "working" : ctxColor === "amber" ? "auth-fail" : "blocked") + ')"></div></div>';
} else {
statsHtml += statRow("context", "\u2014");
$focusStats.innerHTML = statsHtml;
}
$focusStats.innerHTML = statsHtml;

// Diff
var d = data.diff || {};
Expand Down
254 changes: 232 additions & 22 deletions src/automation/mod.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/budget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ mod tests {
persistent: false,
memory: None,
env: HashMap::new(),
role: None,
}],
tool_packs: vec![],
workflows: vec![],
Expand All @@ -271,6 +272,7 @@ mod tests {
agent_weekly_tokens: caps,
}),
webhooks: vec![],
roles: None,
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/cli/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ pub fn run(agent_ref: &str, lines: u32, json: bool) -> Result<()> {
let resolved = super::agent_ref::resolve(agent_ref)?;
let agent = resolved.agent_config()?;
let runtime_name = agent
.resolved_runtime(&resolved.config.defaults)
.unwrap_or_else(|| "unknown".to_string());
.resolved_runtime(&resolved.config.defaults, &resolved.config.roles)
.ok_or_else(|| {
TuttiError::ConfigValidation(format!(
"agent '{}' has no runtime — set 'runtime' on the agent, assign a 'role' \
with a [roles] mapping, or set 'defaults.runtime' in tutti.toml",
resolved.agent_name
))
})?;
let session = TmuxSession::session_name(&resolved.workspace_name, &resolved.agent_name);

ensure_session_running(&session, &resolved.agent_name)?;
Expand Down
8 changes: 5 additions & 3 deletions src/cli/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ fn evaluate_checks(

let launch_targets_supported_runtime = config.agents.iter().any(|agent| {
agent
.resolved_runtime(&config.defaults)
.resolved_runtime(&config.defaults, &config.roles)
.as_deref()
.is_some_and(|rt| matches!(rt, "claude-code" | "codex" | "openclaw" | "aider"))
});
Expand Down Expand Up @@ -185,7 +185,7 @@ fn evaluate_checks(
&& policy_configured
&& config.agents.iter().any(|agent| {
agent
.resolved_runtime(&config.defaults)
.resolved_runtime(&config.defaults, &config.roles)
.as_deref()
.is_some_and(|rt| matches!(rt, "codex" | "openclaw" | "aider"))
})
Expand All @@ -198,7 +198,7 @@ fn evaluate_checks(
}

for agent in &config.agents {
let Some(runtime_name) = agent.resolved_runtime(&config.defaults) else {
let Some(runtime_name) = agent.resolved_runtime(&config.defaults, &config.roles) else {
checks.push(DoctorCheck {
check: format!("runtime/{}", agent.name),
status: DoctorStatus::Fail,
Expand Down Expand Up @@ -592,6 +592,7 @@ mod tests {
persistent: false,
memory: None,
env: HashMap::new(),
role: None,
}],
tool_packs: vec![],
workflows: vec![],
Expand All @@ -600,6 +601,7 @@ mod tests {
observe: None,
budget: None,
webhooks: vec![],
roles: None,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/cli/down.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub fn run(
project_root: project_root.to_path_buf(),
agent_name: agent.name.clone(),
runtime: agent
.resolved_runtime(&config.defaults)
.resolved_runtime(&config.defaults, &config.roles)
.unwrap_or_else(|| "—".to_string()),
session_name: session.clone(),
reason: "manual".to_string(),
Expand Down Expand Up @@ -177,7 +177,7 @@ fn run_all(clean: bool) -> Result<()> {
project_root: project_root.to_path_buf(),
agent_name: agent.name.clone(),
runtime: agent
.resolved_runtime(&config.defaults)
.resolved_runtime(&config.defaults, &config.roles)
.unwrap_or_else(|| "—".to_string()),
session_name: session.clone(),
reason: "manual".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions src/cli/handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ mod tests {
persistent: false,
memory: None,
env: HashMap::new(),
role: None,
}],
tool_packs: vec![],
workflows: vec![],
Expand All @@ -586,6 +587,7 @@ mod tests {
observe: None,
budget: None,
webhooks: vec![],
roles: None,
}
}

Expand Down
Loading
Loading