Skip to content

Commit a5b7bb1

Browse files
committed
fix: prevent tunnel logs from corrupting TUI
1 parent 04b1747 commit a5b7bb1

14 files changed

Lines changed: 268 additions & 64 deletions

File tree

.wcode/design/acceptance.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@
359359
- schema_version: 1
360360
id: AC-UX-001
361361
title: Operator surfaces remain usable across terminal layouts
362-
statement: The TUI stays readable in narrow layouts, uses the documentation palette and a compact connection/status hierarchy, actively loads Intelligence for the focused project, keeps the pairing code visible after connection, shows the complete supported-command catalog, distinguishes installed LSP availability from launch-ready trust and live-validated sessions, and exposes real warm LSP session/document/start/query telemetry. The TUI and WebUI label Executable access separately from Exact repository operation while preserving per-request approve/deny behavior. The WebUI renders a bounded project file tree and largest-file list with the repository line limit visible.
362+
statement: The TUI stays readable in narrow layouts, uses the documentation palette and a compact connection/status hierarchy, actively loads Intelligence for the focused project, keeps the pairing code visible after connection, shows the complete supported-command catalog, distinguishes installed LSP availability from launch-ready trust and live-validated sessions, exposes real warm LSP session/document/start/query telemetry, and claims terminal output for the lifetime of the alternate-screen renderer so background operator diagnostics are retained as structured state without bypassing ratatui. The TUI and WebUI label Executable access separately from Exact repository operation while preserving per-request approve/deny behavior. The WebUI renders a bounded project file tree and largest-file list with the repository line limit visible.
363363
verification:
364364
- kind: test
365365
path: tests/unit/ui/monitor.rs
@@ -376,6 +376,9 @@
376376
- kind: test
377377
path: tests/unit/ui/monitor.rs
378378
symbol: semantic_session_status_updates_operator_intelligence_state
379+
- kind: test
380+
path: tests/unit/ui/monitor.rs
381+
symbol: dashboard_terminal_claim_suppresses_background_operator_output
379382
- kind: test
380383
path: tests/unit/ui/monitor.rs
381384
symbol: opening_intelligence_loads_the_focused_workspace_without_prior_mcp_calls
@@ -527,8 +530,11 @@
527530
- schema_version: 1
528531
id: AC-TUNNEL-001
529532
title: Managed tunnel selection supports free provider fallback
530-
statement: Every candidate tunnel is instance-health-verified, provider parsing rejects control hosts, and OAuth metadata plus existing sessions follow only the verified aliases owned by the running wcode process.
533+
statement: Every candidate tunnel is instance-health-verified, provider parsing rejects control hosts, OAuth metadata plus existing sessions follow only the verified aliases owned by the running wcode process, and managed tunnel/provider dependency paths never write directly to stdout/stderr or inherit child terminal output; diagnostics flow through TaskMonitor so an active alternate-screen TUI cannot be corrupted.
531534
verification:
535+
- kind: test
536+
path: tests/unit/runtime/tunnel/mod.rs
537+
symbol: tunnel_runtime_never_writes_directly_to_the_terminal
532538
- kind: test
533539
path: tests/unit/runtime/tunnel/mod.rs
534540
symbol: parses_quick_tunnel_url

.wcode/design/components.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@
280280
- use revision-exact Evidence for current Proof and expose explicit stable/changing/needs_convergence/incomplete requirement states with blockers
281281
- compare the complete declared component architecture with code-derived dependency edges, distinguish strong observed drift from evidence gaps, and retain code statistics, mapped Git changes and structural revision history without reverting to an undifferentiated node-ball graph
282282
- keep English as the default UI language and expose explicit manual English / Simplified Chinese switching in both TUI and WebUI without locale auto-detection
283-
- keep the TUI visually compact with the documentation palette, actively load the focused Workspace when Intelligence opens, retain the pairing code after connection, expose the complete supported-command catalog, and distinguish LSP available / launch-ready / live-validated state alongside real warm semantic-session/document/start/query telemetry rather than a decorative LSP state
283+
- keep the TUI visually compact with the documentation palette, actively load the focused Workspace when Intelligence opens, retain the pairing code after connection, expose the complete supported-command catalog, distinguish LSP available / launch-ready / live-validated state alongside real warm semantic-session/document/start/query telemetry rather than a decorative LSP state, and centrally claim terminal output while ratatui owns the alternate screen so background runtime diagnostics cannot punch through the dashboard
284284
- expose W as the discoverable TUI shortcut for the protected Project Observatory and provide TUI/WebUI controls for selectable pending authorization requests plus protected WebUI project and command-access management while keeping executable access distinct from exact repository-operation approval
285285
- derive a bounded project file tree and largest-file list from the same Software Graph snapshot, expose the 1000-line repository limit, and report truncation instead of starting an unbounded WebUI filesystem scan
286286
constraints:
@@ -664,6 +664,7 @@
664664
- start Cloudflare Quick Tunnel, localhost.run, Pinggy and Tailscale Funnel concurrently in the background so the dashboard never waits for a provider; retry unreachable providers on a fixed interval and keep every instance-health-verified tunnel live
665665
- respawn a single dead tunnel after an exponential backoff without restarting the process or disturbing live tunnels, promoting the next live tunnel when the primary endpoint dies
666666
- keep explicit provider selection and stable --public-url overrides available without weakening OAuth or local health verification
667+
- route provider attempts, retries, dependency/install diagnostics, stale-process reclamation and connection events through TaskMonitor operator messages; never write directly to the terminal or inherit installer stdio while the runtime may be rendering the TUI
667668
- isolate provider lifecycle, dependency checks, URL parsing and health verification from the top-level process bootstrap
668669
depends_on:
669670
- component:mcp-runtime

.wcode/design/requirements.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@
364364
- schema_version: 1
365365
id: REQ-UX-001
366366
title: Operator-visible Software Intelligence
367-
intent: TUI and protected local WebUI surfaces must expose live software-intelligence state in a compact hierarchy that uses the documentation palette and remains usable in narrow terminals. The TUI actively loads the focused project, keeps the pairing code visible after connection, and exposes the complete command catalog. The WebUI shows a bounded project file tree and the largest files, including the repository line-limit signal. Executable access remains distinct from exact repository-operation approval, while language, appearance, refresh, project, precision, connection and per-request approval controls stay explicit.
367+
intent: TUI and protected local WebUI surfaces must expose live software-intelligence state in a compact hierarchy that uses the documentation palette and remains usable in narrow terminals. The TUI actively loads the focused project, keeps the pairing code visible after connection, exposes the complete command catalog, and owns stdout/stderr while its alternate screen is active so background tunnel/setup diagnostics cannot corrupt the rendered terminal. The WebUI shows a bounded project file tree and the largest files, including the repository line-limit signal. Executable access remains distinct from exact repository-operation approval, while language, appearance, refresh, project, precision, connection and per-request approval controls stay explicit.
368368
priority: high
369369
implemented_by:
370370
- component:intelligence-ui
@@ -511,7 +511,7 @@
511511
- schema_version: 1
512512
id: REQ-TUNNEL-001
513513
title: Public tunnel availability is provider-resilient
514-
intent: Remote MCP startup must not depend on one tunnel vendor. wcode keeps every instance-health-verified tunnel live, self-heals failures without restarting the process, directs OAuth to the hostname receiving the request, and preserves scoped persisted sessions when clients reconnect through another verified alias or restart the runtime.
514+
intent: Remote MCP startup must not depend on one tunnel vendor. wcode keeps every instance-health-verified tunnel live, self-heals failures without restarting the process, directs OAuth to the hostname receiving the request, preserves scoped persisted sessions when clients reconnect through another verified alias or restart the runtime, and routes background provider diagnostics through structured monitor state rather than direct terminal writes while the TUI owns the terminal.
515515
priority: high
516516
implemented_by:
517517
- component:managed-tunnel

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ wcode --workspace "$PWD"
4646

4747
The TUI opens immediately. In the normal configuration wcode also starts the
4848
local HTTP service, protected WebUI, OAuth server, and managed HTTPS tunnels.
49-
Every tunnel is accepted only after its public `/healthz` response matches the
50-
current runtime instance.
49+
While the alternate-screen dashboard is active it owns terminal output:
50+
background tunnel/setup diagnostics are captured into `TaskMonitor` state
51+
instead of writing over the ratatui frame. Every tunnel is accepted only after
52+
its public `/healthz` response matches the current runtime instance.
5153

5254
The selected root remains the security boundary. Project markers below it are
5355
discovered as subspaces, so a broad root such as `~/Code` can safely expose

docs/manual/releases/v0.5.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ v0.5.0 also changes when semantic work is recommended and how operators can tell
8585
- `agent_context` detects relationship-oriented tasks such as references, callers, implementations, rename impact, and cross-file semantic impact. It recommends `semantic_navigation` only when stronger relationship evidence is useful.
8686
- Ordinary “where is this symbol?” work stays on Tree-sitter/search instead of paying LSP startup or query cost.
8787
- The TUI Intelligence view exposes warm-session count, synchronized document count, provider starts, semantic query count, and fresh/stale semantic provider state.
88+
- The live dashboard now owns terminal output while its alternate screen is active. Tunnel/provider retries, dependency checks, reconnects, Setup-browser diagnostics, and other background operator messages are routed through `TaskMonitor` instead of writing directly to stdout/stderr; provider installers are captured rather than inheriting the terminal. This prevents background tunnel text from floating over or corrupting the ratatui frame.
8889
- Cached graph inputs can avoid graph reconstruction while still warming the provider session so later semantic requests do not pay process startup cost.
8990
- High-value Call Hierarchy and Implementation expansion remains bounded rather than querying every variable, field, and low-value symbol.
9091
- `semantic_provider_refresh` remains available as a force-refresh surface; automatic maintenance is the default for the hardened lane.

docs/manual/releases/v0.5.0.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ v0.5.0 也改变了何时推荐 Semantic Work,以及 Operator 怎么判断 War
8585
- `agent_context` 会识别 Reference、Caller、Implementation、Rename Impact、Cross-file Semantic Impact 等 Relationship-oriented Task,只有更强关系证据有价值时才推荐 `semantic_navigation`
8686
- 普通“这个 Symbol 在哪里”继续走 Tree-sitter/Search,不承担 LSP Startup / Query Cost。
8787
- TUI Intelligence 会显示 Warm Session 数、已同步 Document 数、Provider Start 次数、Semantic Query 数以及 Fresh/Stale Provider State。
88+
- Live Dashboard 进入 Alternate Screen 后会独占 Terminal Output。Tunnel/Provider Retry、Dependency Check、Reconnect、Setup Browser Diagnostic 等后台 Operator Message 全部先进入 `TaskMonitor`,不再直接写 stdout/stderr;Provider Installer 也改为捕获输出而不是继承终端,避免隧道日志漂在 ratatui 画面上把 TUI 打花。
8889
- Graph Input 未变化时可以跳过 Graph Reconstruction,同时仍保持 Provider Session Warm,让后续 Semantic Request 不必再次支付 Process Startup Cost。
8990
- Call Hierarchy / Implementation 只扩展高价值 Symbol,并继续保持有界,不会为每个 Variable / Field 发大量请求。
9091
- `semantic_provider_refresh` 继续保留为 Force-refresh Surface;Hardened Lane 默认由 Runtime 自动维护。

src/app/mod.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::auth::AuthState;
22
use crate::harness::ToolHarness;
33
use crate::mcp::AppState;
4-
use crate::monitor::{MonitorConfig, MonitorRenderer, TaskMonitor};
4+
use crate::monitor::{MonitorConfig, MonitorRenderer, OperatorMessageKind, TaskMonitor};
55
use crate::tunnel::{
66
normalize_public_url, public_endpoint_health_loop, spawn_tunnel_supervisor,
77
wait_for_public_endpoint, ActiveTunnel, TunnelEvent, TunnelProvider,
@@ -459,7 +459,11 @@ pub async fn run() -> Result<()> {
459459
let _ = tunnel_settled_tx.send(true);
460460
local_url.clone()
461461
} else {
462-
println!(" ↗ tunnel requesting HTTPS endpoint in the background");
462+
monitor.operator_message(
463+
OperatorMessageKind::Info,
464+
"tunnel",
465+
"requesting HTTPS endpoint in the background",
466+
);
463467
monitor.mark_public_endpoint("pending", None);
464468
{
465469
let forward_tx = tunnel_event_tx.clone();
@@ -546,14 +550,15 @@ pub async fn run() -> Result<()> {
546550
if open_setup {
547551
let mut settled = tunnel_settled_rx.clone();
548552
let url_slot = shared_public_url.clone();
553+
let setup_monitor = monitor.clone();
549554
tokio::spawn(async move {
550555
if !*settled.borrow() {
551556
// Providers keep retrying in the background; do not hold the
552557
// opener hostage if none of them connects quickly.
553558
let _ = timeout(Duration::from_secs(120), settled.changed()).await;
554559
}
555560
let base = url_slot.read().unwrap().clone();
556-
open_setup_hub(&base, &format!("{base}/mcp"));
561+
open_setup_hub(&base, &format!("{base}/mcp"), &setup_monitor);
557562
});
558563
}
559564
let renderer = monitor.spawn_renderer(monitor_config, args.monitor);
@@ -633,7 +638,11 @@ pub async fn run() -> Result<()> {
633638
auth.pairing_code()
634639
);
635640
if let Err(error) = send_imessage(recipient, &message) {
636-
eprintln!(" ! imessage {error:#}");
641+
monitor.operator_message(
642+
OperatorMessageKind::Warning,
643+
"imessage",
644+
format!("{error:#}"),
645+
);
637646
}
638647
}
639648
tunnels.push(active);
@@ -693,7 +702,11 @@ pub async fn run() -> Result<()> {
693702
let dead_provider = tunnels[index].provider();
694703
let dead_url = tunnels[index].public_url().to_owned();
695704
let was_primary = index == 0;
696-
eprintln!(" ! tunnel {reason}; respawning {}", dead_provider.label());
705+
monitor.operator_message(
706+
OperatorMessageKind::Warning,
707+
"tunnel",
708+
format!("{reason}; respawning {}", dead_provider.label()),
709+
);
697710
let mut dead = tunnels.remove(index);
698711
dead.stop().await;
699712
monitor.remove_tunnel(&dead_url);
@@ -723,9 +736,13 @@ pub async fn run() -> Result<()> {
723736
.and_modify(|count| *count += 1)
724737
.or_insert(1);
725738
let backoff_secs = (15u64 << (*deaths - 1).min(4)).min(300);
726-
eprintln!(
727-
" ! tunnel {} respawns in {backoff_secs}s (death #{deaths})",
728-
dead_provider.label()
739+
monitor.operator_message(
740+
OperatorMessageKind::Warning,
741+
"tunnel",
742+
format!(
743+
"{} respawns in {backoff_secs}s (death #{deaths})",
744+
dead_provider.label()
745+
),
729746
);
730747
pending_respawns.push((
731748
dead_provider,
@@ -827,9 +844,13 @@ async fn wait_for_monitor_interrupt(receiver: Option<watch::Receiver<bool>>) {
827844
}
828845
}
829846

830-
fn open_setup_hub(setup_url: &str, mcp_url: &str) {
831-
println!(" ↗ setup opening wcode setup hub");
832-
println!(" · MCP URL {mcp_url}");
847+
fn open_setup_hub(setup_url: &str, mcp_url: &str, monitor: &TaskMonitor) {
848+
monitor.operator_message(
849+
OperatorMessageKind::Info,
850+
"setup",
851+
"opening wcode setup hub",
852+
);
853+
monitor.operator_message(OperatorMessageKind::Info, "MCP URL", mcp_url);
833854
let mut command = if cfg!(target_os = "macos") {
834855
let mut command = StdCommand::new("open");
835856
command.arg(setup_url);
@@ -849,7 +870,11 @@ fn open_setup_hub(setup_url: &str, mcp_url: &str) {
849870
.stderr(StdStdio::null())
850871
.spawn()
851872
{
852-
eprintln!(" ! setup could not open the browser ({error}); visit {setup_url}");
873+
monitor.operator_message(
874+
OperatorMessageKind::Warning,
875+
"setup",
876+
format!("could not open the browser ({error}); visit {setup_url}"),
877+
);
853878
}
854879
}
855880

0 commit comments

Comments
 (0)