Skip to content
Merged
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
110 changes: 87 additions & 23 deletions crates/tui/src/tui/session_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,19 @@ impl SessionBootSurface {
.filter(|row| row.state == McpServerBootState::Connecting)
.map(|row| row.name.as_str())
.collect();
// A server whose login expired is not a failure: the engine already
// knows the remedy (`codewhale mcp login <server>` / the login tool),
// so the chip counts it under the shared auth-required label and only
// calls the rest failed (#5926).
let need_login = self
.servers
.iter()
.filter(|row| row.state == McpServerBootState::NeedsLogin)
.count();
let failed = self
.servers
.iter()
.filter(|row| {
matches!(
row.state,
McpServerBootState::Failed | McpServerBootState::NeedsLogin
)
})
.filter(|row| row.state == McpServerBootState::Failed)
.count();
let connected = self
.servers
Expand All @@ -238,19 +242,32 @@ impl SessionBootSurface {
budget,
);
}
if failed > 0 {
return activity_notice_from_candidates(
SessionBootActivityLevel::Failure,
vec![
format!(
"MCP{ITEM_SEPARATOR}{connected} {}{ITEM_SEPARATOR}{failed} {}",
tr(locale, MessageId::ExtensionsStateConnected),
tr(locale, MessageId::PhaseFailed)
),
format!("MCP{ITEM_SEPARATOR}{failed} failed"),
],
budget,
if failed > 0 || need_login > 0 {
let auth_label = mcp_auth_required_state_label();
let mut full = format!(
"MCP{ITEM_SEPARATOR}{connected} {}",
tr(locale, MessageId::ExtensionsStateConnected)
);
// The narrow form drops the glyph and shortens the verb so both
// counts survive an 80-column footer.
let mut compact = String::from("MCP");
if need_login > 0 {
full.push_str(&format!("{ITEM_SEPARATOR}{need_login} {auth_label}"));
compact.push_str(&format!("{ITEM_SEPARATOR}{need_login} login"));
Comment on lines +255 to +256

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize both authentication-chip variants

When app.ui_locale is non-English, this new branch emits English in both candidates: mcp_auth_required_state_label() returns ◆ auth required, while the compact form hardcodes login; only the surrounding connected/failed labels are translated. This produces mixed-language wide and narrow footers even though localized LaunchMcpNeedsSignInOne/LaunchMcpNeedsSignInMany messages already exist. Compose the authentication prose through tr(locale, MessageId::...), leaving only the glyph in code.

AGENTS.md reference: crates/tui/AGENTS.md:L25-L26

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the compact state neutral to the authentication method

When a needs-auth server uses a manual bearer/environment header or is plugin-contributed, the compact candidate incorrectly labels it login. These servers still set McpServerSnapshot::auth_required after a 401, but auth_required_recovery_hint() explicitly routes them to correcting the credential and /mcp reload, while OAuth login is disabled. At narrow widths this therefore advertises the wrong recovery class; use a neutral compact label such as auth/auth required, or derive the wording from the server's supported recovery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] Compact auth-required label is not localized

The new compact fallback hard-codes the string "login" when need_login > 0. The full candidate localizes connected/failed via tr(locale, ...), so non-English TUI users can see an English word in the narrow footer. If an existing MessageId for "login" exists, use it; otherwise a new localized message is needed for the compact form.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Compact login-only path is not directly tested

The updated single-state test uses a 100-column budget and asserts the full candidate. The new compact candidate for need_login > 0 && failed == 0 (MCP · N login) is not exercised by a test; the mixed-state test does cover the compact failed+login combination. A narrow-budget assertion for the login-only case would lock the intended fallback.

Comment on lines +254 to +256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Login status bypasses localization

The new login labels remain English while adjacent status labels use tr. Non-English footers therefore mix languages.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Compact chip hardcodes English 'login'

The narrow candidate uses compact.push_str(&format!("{ITEM_SEPARATOR}{need_login} login"));, which hardcodes English 'login'. The full candidate uses the shared mcp_auth_required_state_label(), and connected/failed are translated in the full form. If the TUI is localized, the compact footer will show an English word; consider using an existing localized message or abbreviation if one is available.

}
if failed > 0 {
full.push_str(&format!(
"{ITEM_SEPARATOR}{failed} {}",
tr(locale, MessageId::PhaseFailed)
));
compact.push_str(&format!("{ITEM_SEPARATOR}{failed} failed"));
}
let level = if failed > 0 {
SessionBootActivityLevel::Failure
} else {
SessionBootActivityLevel::Attention
};
return activity_notice_from_candidates(level, vec![full, compact], budget);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep real failures visible at the 40-column budget

At the supported 40-column layout, notice_budget is 20, but with both counts nonzero the shortest candidate, MCP · 1 login · 1 failed, is 24 columns, so activity_notice_from_candidates() returns None and the footer hides the actual failure. Before this change the same state fell back to MCP · 2 failed, which fit in 14 columns. Add another fallback that sheds the auth clause and retains the higher-severity failed count instead of dropping the entire notice.

AGENTS.md reference: crates/tui/AGENTS.md:L22-L22

Useful? React with 👍 / 👎.

}
if self.phase == SessionBootPhase::Booting {
let count = self.servers.len().max(self.unnamed_connecting);
Expand Down Expand Up @@ -644,12 +661,59 @@ mod tests {
.map(|row| row.state),
Some(McpServerBootState::NeedsLogin)
);
// The compact activity chip counts a needs-login server with the
// failure band (the receipt-line renderer this pinned moved to the
// chip in the Tideline boot-surface refactor).
// The compact activity chip names a needs-login server under the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Test comment says 'compact' but exercises full-width candidate

The updated test comment says 'The compact activity chip names a needs-login server...' but the assertion calls activity_notice(Locale::En, 100) and verifies the full candidate including the connected count. The wording is misleading.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test calls activity_notice with width 100 and asserts the full candidate, so the comment should say full-width rather than compact.

Suggested change
// The compact activity chip names a needs-login server under the
// The full-width activity chip names a needs-login server under the

// shared auth-required label, at attention level: the remedy is a
// login, not a repair (#5926).
assert_eq!(surface.servers.len(), 1);
let chip = surface.activity_notice(Locale::En, 100);
assert!(chip.is_some(), "needs-login must surface on the boot chip");
let chip = surface
.activity_notice(Locale::En, 100)
.expect("needs-login must surface on the boot chip");
assert_eq!(chip.level, SessionBootActivityLevel::Attention);
assert_eq!(
chip.text,
format!(
"MCP{ITEM_SEPARATOR}0 connected{ITEM_SEPARATOR}1 {}",
mcp_auth_required_state_label()
)
);
assert!(!chip.text.contains("failed"), "{}", chip.text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Compact needs-login-only branch lacks explicit coverage

The mixed-state test covers the compact branch when both login and failure counts are present, and the first needs-login test covers the full-width branch. The compact path for need_login > 0 && failed == 0 (MCP · N login) is not explicitly asserted, despite the PR description implying both widths are covered for the single-state case.

}

#[test]
fn chip_separates_expired_logins_from_real_failures() {
let mut expired = server("slack", true, false, Some("401 Unauthorized"));
expired.auth_required = true;
let snap = snapshot(vec![
server("alpha", true, true, None),
expired,
server("beta", true, false, Some("Stdio transport closed")),
]);
let surface = SessionBootSurface::from_parts(
Some(&snap),
false,
&[],
3,
PluginBootSummary::default(),
);
let chip = surface
.activity_notice(Locale::En, 100)
.expect("mixed states surface on the boot chip");
assert_eq!(chip.level, SessionBootActivityLevel::Failure);
assert_eq!(
chip.text,
format!(
"MCP{ITEM_SEPARATOR}1 connected{ITEM_SEPARATOR}1 {}{ITEM_SEPARATOR}1 failed",
mcp_auth_required_state_label()
)
);
// Under a tight budget the compact form keeps both counts.
let compact = surface
.activity_notice(Locale::En, 30)
.expect("compact chip");
assert_eq!(
compact.text,
format!("MCP{ITEM_SEPARATOR}1 login{ITEM_SEPARATOR}1 failed")
);
}

#[test]
Expand Down
Loading