Skip to content

Commit 34162b7

Browse files
0xrinegadeclaude
andcommitted
feat(tui): Enhanced wallet explorer UX with depth control
Major improvements to the TUI wallet explorer interface: **Canvas-Based Graph Rendering:** - Replaced tui-nodes with Canvas for 10K+ node scalability - Implemented hierarchical layout (inflows left, target center, outflows right) - Added viewport system with pan (WASD) and zoom (+/-) - Fixed graph not rendering (was showing as list) **Enhanced Visibility:** - Changed edges to bright cyan (Color::Cyan) for maximum visibility - Increased node size from radius 2.0 → 3.0 - Doubled pan speed (10.0 → 20.0 units) for smoother navigation - All node labels now display (not just selected) **Dynamic BFS Depth Control:** - Added max_depth (1-20) and current_depth tracking - Implemented [ and ] keybindings to adjust exploration depth - Graph title shows "depth:current/max" status - Updated help overlay with depth control instructions - Bounds checking prevents unrealistic depth values **Help System:** - Scrollable help overlay with j/k navigation - Shows scroll position "Help (1/36)" - Added comprehensive keybinding reference - Updated About section with depth range info **Technical Changes:** - GraphInput enum: Added IncreaseDepth/DecreaseDepth variants - WalletGraph: Added max_depth/current_depth fields - Compute hierarchical layout on-demand in render() - Non-blocking try_lock() prevents UI freezing - Fixed MCP response parsing for nested JSON This addresses all UX issues: graph visualization, edge visibility, navigation controls, and exploration depth management. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0c21c67 commit 34162b7

File tree

5 files changed

+1576
-486
lines changed

5 files changed

+1576
-486
lines changed

src/commands/research.rs

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -462,27 +462,34 @@ fn extract_summary_json(result: &ovsm::Value) -> Result<String> {
462462
/// Format the wallet analysis summary using AI
463463
async fn format_wallet_analysis(ai_service: &mut AiService, wallet: &str, summary_json: &str) -> Result<String> {
464464
// System prompt for custom formatting (bypass planning mode)
465-
let system_prompt = r#"You are a markdown formatter. The blockchain analysis is ALREADY COMPLETE.
465+
let system_prompt = r#"You are a blockchain detective presenting findings. Format the JSON data as an engaging terminal report.
466466
467-
Your ONLY job: Convert the provided JSON into clean markdown.
467+
CRITICAL RULES:
468+
1. NEVER use <br> or HTML tags - use NEWLINES only
469+
2. Keep addresses SHORT (first 35 chars + "...")
470+
3. Be entertaining but concise
468471
469-
DO NOT analyze, interpret, or add commentary. ONLY format what's given:
472+
FORMAT:
473+
🔍 WALLET INTEL: [wallet]
474+
═══════════════════════════════════════════════════
470475
471-
**Wallet Summary**
472-
- Total: {total_transfers_unique} unique transfers ({inflow_count} in, {outflow_count} out)
473-
- Tokens: {num_tokens} different tokens
476+
📊 Activity Snapshot
477+
Total moves: X unique transfers (Y in, Z out)
478+
• Token variety: N different tokens
474479
475-
**Top 3 Senders** (addresses sending TO this wallet)
476-
| Address | Tokens Sent |
477-
|---------|-------------|
478-
[For each sender in top_senders array, list address and all tokens with amounts]
480+
💰 Top 3 Senders (money flowing IN)
481+
┌───────────────────────────────────┬────────────────────────────────────────┐
482+
│ Address │ Tokens Sent │
483+
├───────────────────────────────────┼────────────────────────────────────────┤
484+
│ [35 chars...] │ TOKEN1: 1,234.56 │
485+
│ │ TOKEN2: 789.01 │
486+
└───────────────────────────────────┴────────────────────────────────────────┘
487+
(Each token on its own line, NO <br> tags!)
479488
480-
**Top 3 Receivers** (addresses receiving FROM this wallet)
481-
| Address | Tokens Received |
482-
|---------|-----------------|
483-
[For each receiver in top_receivers array, list address and all tokens with amounts]
489+
📤 Top 3 Receivers (money flowing OUT)
490+
[Same table format]
484491
485-
NO analysis. NO interpretation. ONLY formatting the JSON into tables."#.to_string();
492+
Add a witty one-liner observation at the end based on the flow pattern."#.to_string();
486493

487494
// Question contains the actual data
488495
let question = format!(r#"Wallet Address: {}
@@ -556,7 +563,10 @@ async fn handle_tui_research(matches: &ArgMatches, wallet: &str) -> Result<()> {
556563
let logs = Arc::clone(&app.logs);
557564
let status = Arc::clone(&app.status);
558565
let phase = Arc::clone(&app.phase);
566+
let wallet_graph = app.get_graph_handle();
567+
let (token_volumes, transfer_events) = app.get_analytics_handles();
559568
let wallet_clone = wallet.to_string();
569+
let target_wallet_for_callback = wallet.to_string();
560570

561571
// Get Tokio runtime handle for spawning async tasks
562572
let runtime_handle = tokio::runtime::Handle::current();
@@ -583,12 +593,91 @@ async fn handle_tui_research(matches: &ArgMatches, wallet: &str) -> Result<()> {
583593

584594
*status.lock().unwrap() = "Initializing MCP tools...".to_string();
585595

586-
// Create research agent
587-
let agent = ResearchAgent::new(
596+
// Create callbacks for TUI updates
597+
let agent_output_clone = Arc::clone(&agent_output);
598+
let output_callback: crate::services::research_agent::TuiCallback = Arc::new(move |msg: &str| {
599+
if let Ok(mut output) = agent_output_clone.lock() {
600+
output.push(msg.to_string());
601+
if output.len() > 500 {
602+
output.drain(0..250);
603+
}
604+
}
605+
});
606+
607+
let logs_clone = Arc::clone(&logs);
608+
let logs_callback: crate::services::research_agent::TuiCallback = Arc::new(move |msg: &str| {
609+
if let Ok(mut log) = logs_clone.lock() {
610+
log.push(format!("[{}] {}", chrono::Local::now().format("%H:%M:%S"), msg));
611+
if log.len() > 1000 {
612+
log.drain(0..500);
613+
}
614+
}
615+
});
616+
617+
// Create graph callback to update TUI wallet graph AND analytics
618+
let target_w = target_wallet_for_callback.clone();
619+
let graph_callback: crate::services::research_agent::GraphCallback = Arc::new(move |from: &str, to: &str, amount: f64, token: &str, timestamp: &str| {
620+
// Update graph
621+
if let Ok(mut graph) = wallet_graph.lock() {
622+
use crate::utils::tui::graph::WalletNodeType;
623+
graph.add_transfer(
624+
from.to_string(),
625+
to.to_string(),
626+
amount,
627+
token.to_string(),
628+
WalletNodeType::Funding,
629+
WalletNodeType::Recipient,
630+
);
631+
}
632+
633+
// Update token volumes
634+
if let Ok(mut vols) = token_volumes.lock() {
635+
if let Some(existing) = vols.iter_mut().find(|v| v.symbol == token) {
636+
existing.amount += amount;
637+
} else {
638+
vols.push(crate::utils::tui::app::TokenVolume {
639+
symbol: token.to_string(),
640+
amount,
641+
});
642+
}
643+
// Sort by volume descending
644+
vols.sort_by(|a, b| b.amount.partial_cmp(&a.amount).unwrap_or(std::cmp::Ordering::Equal));
645+
}
646+
647+
// Update transfer events with HISTORICAL timestamp
648+
if let Ok(mut evts) = transfer_events.lock() {
649+
let direction = if to == target_w { "IN" } else { "OUT" };
650+
// Use historical timestamp if available, else current time
651+
let ts = if !timestamp.is_empty() && timestamp.len() >= 10 {
652+
timestamp[..10].to_string() // Just the date
653+
} else {
654+
chrono::Local::now().format("%Y-%m-%d").to_string()
655+
};
656+
evts.push(crate::utils::tui::app::TransferEvent {
657+
timestamp: ts,
658+
amount,
659+
token: token.to_string(),
660+
direction: direction.to_string(),
661+
});
662+
// Sort by timestamp descending (newest first) and keep last 50
663+
evts.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
664+
evts.truncate(50);
665+
}
666+
});
667+
668+
let tui_callbacks = crate::services::research_agent::TuiCallbacks {
669+
output: Some(output_callback),
670+
logs: Some(logs_callback),
671+
graph: Some(graph_callback),
672+
};
673+
674+
// Create research agent with TUI callbacks
675+
let agent = ResearchAgent::new_with_callbacks(
588676
ai_service,
589677
ovsm_service,
590678
Arc::clone(&mcp_arc),
591-
wallet_clone.clone()
679+
wallet_clone.clone(),
680+
tui_callbacks
592681
);
593682

594683
// Add initialization complete message
@@ -600,11 +689,12 @@ async fn handle_tui_research(matches: &ArgMatches, wallet: &str) -> Result<()> {
600689
*phase.lock().unwrap() = "PLANNING".to_string();
601690
*status.lock().unwrap() = "AI generating investigation plan...".to_string();
602691

603-
// Run investigation (this will take a while)
692+
// Run investigation in AUTO mode (deterministic, no AI prompts)
604693
*phase.lock().unwrap() = "INVESTIGATING".to_string();
605-
*status.lock().unwrap() = "Running blockchain queries...".to_string();
694+
*status.lock().unwrap() = "BFS graph expansion starting...".to_string();
606695

607-
match agent.investigate().await {
696+
// Use investigate_auto for TUI mode - no AI, just deterministic graph building
697+
match agent.investigate_auto().await {
608698
Ok(report) => {
609699
*phase.lock().unwrap() = "COMPLETE".to_string();
610700
*status.lock().unwrap() = "Investigation complete!".to_string();

src/services/graph_tui.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ use cursive::traits::*;
1313
use cursive::views::{
1414
Dialog, LinearLayout, Panel, ScrollView, SelectView, TextView,
1515
};
16+
use cursive::event::Key;
1617
use cursive::{Cursive, CursiveExt};
1718
use std::collections::{HashMap, HashSet};
19+
use std::sync::{Arc, Mutex};
1820

1921
use super::research_agent::{Transfer, TransferGraph};
2022

23+
/// Shared state wrapper for callbacks
24+
type SharedState = Arc<Mutex<GraphViewerState>>;
25+
2126
/// Interactive TUI state for graph navigation
2227
#[derive(Clone)]
2328
pub struct GraphViewerState {

0 commit comments

Comments
 (0)