Skip to main content

Mountain/Vine/Server/Notification/
OutputChannelAppend.rs

1//! Cocoon → Mountain `outputChannel.append` notification.
2//! Twin of `output.append`; see `OutputCreate.rs` for the duplicate-wire
3//! rationale.
4
5use serde_json::Value;
6use tauri::Emitter;
7
8use crate::{Vine::Server::MountainVinegRPCService::MountainVinegRPCService, dev_log};
9
10pub async fn OutputChannelAppend(Service:&MountainVinegRPCService, Parameter:&Value) {
11	let ChannelName = Parameter
12		.get("channel")
13		.or_else(|| Parameter.get("name"))
14		.and_then(Value::as_str)
15		.unwrap_or("?");
16
17	// Try the per-channel coalescer first. The git extension's
18	// `[OperationManager]` traces and the typescript-language-features
19	// `[Info  - …]` lines arrive in tight 30+/100ms bursts; coalescing
20	// at a 50ms window collapses the IPC + Sky emit count without
21	// reordering text within a channel.
22	//
23	// Falls through to the legacy per-append path when:
24	//   - `OutputCoalesce=0` is set (debug escape hatch).
25	//   - The payload has no `value` field to buffer.
26	let TextValue = Parameter.get("value").and_then(Value::as_str);
27
28	let CoalesceEnqueued = match TextValue {
29		Some(Text) => {
30			super::OutputChannelCoalesce::TryEnqueue(
31				Service.ApplicationHandle(),
32				ChannelName.to_string(),
33				Text.to_string(),
34			)
35		},
36
37		None => false,
38	};
39
40	if CoalesceEnqueued {
41		return;
42	}
43
44	let _ = Service.ApplicationHandle().emit("sky://output/append", Parameter);
45
46	// Legacy per-append fire path - only reached under the disable hook
47	// or when the payload is missing a `value` field. The coalescer
48	// above is the steady-state path. Tags below preserve the previous
49	// channel-aware routing (Git/SCM at `grpc`, everything else at
50	// `output-verbose`).
51
52	// Char-aware truncation. Slicing a `&str` at `&S[..200]` panics when
53	// byte 200 lands inside a multi-byte UTF-8 codepoint (vscode.git's
54	// progress messages contain `•` which is 3 bytes; if the message is
55	// >200 bytes and the bullet sits across the boundary, the slice
56	// crashes the tokio worker - observed live during SCM viewlet open).
57	// Walk char boundaries instead so the cut always lands between codepoints.
58	let TruncatedValue = Parameter
59		.get("value")
60		.and_then(Value::as_str)
61		.map(|S| {
62			if S.len() > 200 {
63				let CutAt = S
64					.char_indices()
65					.map(|(Index, _)| Index)
66					.take_while(|Index| *Index <= 200)
67					.last()
68					.unwrap_or(0);
69				format!("{}…", &S[..CutAt])
70			} else {
71				S.to_string()
72			}
73		})
74		.unwrap_or_else(|| "<no-value>".to_string());
75
76	if ChannelName.eq_ignore_ascii_case("git")
77		|| ChannelName.eq_ignore_ascii_case("source control")
78		|| ChannelName.eq_ignore_ascii_case("scm")
79	{
80		dev_log!(
81			"grpc",
82			"[OutputChannel:{}] {}",
83			ChannelName,
84			TruncatedValue.trim_end_matches('\n')
85		);
86	} else {
87		dev_log!("output-verbose", "[OutputChannel] append channel={}", ChannelName);
88	}
89}