feat: display name in banner + prompt

Banner shows human-readable principal and DID on separate lines:
  Principal: tking@guildhouse.dev
  DID:       did:web:guildhouse.dev/user/tking

Prompt uses short name: [governed] tking@gsh

Reads BASCULE_DISPLAY_NAME env. Fallback: parse DID to name@domain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tyler J King 2026-04-03 10:18:13 -04:00
parent ff16b5642e
commit 231bed5f92
2 changed files with 30 additions and 4 deletions

View file

@ -150,7 +150,8 @@ fn print_banner(session: &SessionState) {
println!(); println!();
println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue()); println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue());
println!("{} Guildhouse Governed Shell v0.1.0{}", "".bright_blue(), " ".repeat(24).to_string() + &"".bright_blue().to_string()); println!("{} Guildhouse Governed Shell v0.1.0{}", "".bright_blue(), " ".repeat(24).to_string() + &"".bright_blue().to_string());
println!("{} Principal: {:<44}{}", "".bright_blue(), session.principal, "".bright_blue()); println!("{} Principal: {:<44}{}", "".bright_blue(), session.display_name, "".bright_blue());
println!("{} DID: {:<44}{}", "".bright_blue(), session.principal, "".bright_blue());
println!("{} Corpus: {:<44}{}", "".bright_blue(), corpus_short, "".bright_blue()); println!("{} Corpus: {:<44}{}", "".bright_blue(), corpus_short, "".bright_blue());
println!("{} Session: {:<44}{}", "".bright_blue(), format!("{} (expires {})", &session.ac_id[..8.min(session.ac_id.len())], expiry), "".bright_blue()); println!("{} Session: {:<44}{}", "".bright_blue(), format!("{} (expires {})", &session.ac_id[..8.min(session.ac_id.len())], expiry), "".bright_blue());
println!("{} Risk: {:<44}{}", "".bright_blue(), println!("{} Risk: {:<44}{}", "".bright_blue(),
@ -199,10 +200,10 @@ fn build_prompt(session: &SessionState) -> DefaultPrompt {
_ => "[governed]", _ => "[governed]",
}; };
let user = session.principal.split('@').next().unwrap_or(&session.principal); let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name);
DefaultPrompt::new( DefaultPrompt::new(
DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, user)), DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)),
DefaultPromptSegment::Empty, DefaultPromptSegment::Empty,
) )
} }

View file

@ -7,6 +7,7 @@ pub struct SessionState {
pub ac_id: String, pub ac_id: String,
pub corpus_cid: String, pub corpus_cid: String,
pub principal: String, pub principal: String,
pub display_name: String,
pub risk_level: String, pub risk_level: String,
pub started_at: chrono::DateTime<chrono::Utc>, pub started_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>, pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
@ -35,10 +36,15 @@ impl SessionState {
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)); .map(|dt| dt.with_timezone(&chrono::Utc));
// Display name: BASCULE_DISPLAY_NAME env, or derive from DID
let display_name = std::env::var("BASCULE_DISPLAY_NAME")
.unwrap_or_else(|_| display_name_from_did(&principal));
Self { Self {
ac_id: ac.context_id.clone(), ac_id: ac.context_id.clone(),
corpus_cid: corpus_cid.to_string(), corpus_cid: corpus_cid.to_string(),
principal, principal,
display_name,
risk_level: "standard".to_string(), // TODO: read from AC when broker embeds it risk_level: "standard".to_string(), // TODO: read from AC when broker embeds it
started_at: chrono::Utc::now(), started_at: chrono::Utc::now(),
expires_at, expires_at,
@ -51,10 +57,14 @@ impl SessionState {
/// Create a minimal session for ungoverned mode. /// Create a minimal session for ungoverned mode.
pub fn ungoverned(corpus_cid: &str) -> Self { pub fn ungoverned(corpus_cid: &str) -> Self {
let principal = whoami();
let display_name = std::env::var("BASCULE_DISPLAY_NAME")
.unwrap_or_else(|_| principal.clone());
Self { Self {
ac_id: "ungoverned".to_string(), ac_id: "ungoverned".to_string(),
corpus_cid: corpus_cid.to_string(), corpus_cid: corpus_cid.to_string(),
principal: whoami(), principal,
display_name,
risk_level: "ungoverned".to_string(), risk_level: "ungoverned".to_string(),
started_at: chrono::Utc::now(), started_at: chrono::Utc::now(),
expires_at: None, expires_at: None,
@ -82,3 +92,18 @@ fn whoami() -> String {
.or_else(|_| std::env::var("USERNAME")) .or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "operator".to_string()) .unwrap_or_else(|_| "operator".to_string())
} }
/// Derive a human-readable display name from a DID.
/// did:web:guildhouse.dev/user/tking → tking@guildhouse.dev
/// Fallback: return the full DID.
fn display_name_from_did(did: &str) -> String {
if let Some(rest) = did.strip_prefix("did:web:") {
let parts: Vec<&str> = rest.splitn(2, '/').collect();
if parts.len() == 2 {
let domain = parts[0];
let name = parts[1].rsplit('/').next().unwrap_or(parts[1]);
return format!("{}@{}", name, domain);
}
}
did.to_string()
}