feat: add highlighted protocol views and inline operation forms
Reference site / build (push) Canceled after 0s

Expose Rust Text View tokens and safely highlight signatures and JSON results. Add animated Interface and inline Run disclosures with a shared 10ms line cadence, typed argument inputs, and explicit execution.

Update documentation and browser regressions for the new controls and explicit counter-value responses.
This commit is contained in:
2026-10-26 10:12:22 +09:00
parent 41e88e3c40
commit db4c8ba97f
21 changed files with 1453 additions and 129 deletions
+35 -5
View File
@@ -161,10 +161,11 @@ impl BrowserSession {
// Reconstruct requested coverage from fresh Client state, never from Host data.
let observation = self.manual_observation(path, depth)?;
let object = self.fresh_object(path)?;
let text =
wip_text_view::render_object(&object).map_err(|e| ManualError::new("render", e))?;
let tokens =
wip_text_view::tokenize_object(&object).map_err(|e| ManualError::new("render", e))?;
let text = wip_text_view::to_plain_text(&tokens);
Ok(
json!({"path": path, "observation": observation, "textView": text, "cached": cached, "stats": self.manual_stats()}),
json!({"path": path, "observation": observation, "textView": text, "textViewTokens": text_view_tokens(&tokens), "cached": cached, "stats": self.manual_stats()}),
)
}
@@ -221,13 +222,14 @@ impl BrowserSession {
.descriptor
.as_ref()
.ok_or_else(missing_observation)?;
let text = wip_text_view::render_interface(&wip_text_view::Interface {
let tokens = wip_text_view::tokenize_interface(&wip_text_view::Interface {
reference: &reference,
descriptor,
})
.map_err(|e| ManualError::new("render", e))?;
let text = wip_text_view::to_plain_text(&tokens);
Ok(
json!({"reference": reference_json(&reference), "descriptor": descriptor_json(descriptor), "textView": text, "cached": cached, "stats": self.manual_stats()}),
json!({"reference": reference_json(&reference), "descriptor": descriptor_json(descriptor), "textView": text, "textViewTokens": text_view_tokens(&tokens), "cached": cached, "stats": self.manual_stats()}),
)
}
@@ -277,6 +279,34 @@ impl BrowserSession {
}
}
// Display-only DTOs: preserve WIP text verbatim, not HTML or protocol identity.
fn text_view_tokens(tokens: &[wip_text_view::Token]) -> Vec<Json> {
tokens
.iter()
.map(|token| json!({"kind": text_view_token_kind(token.kind()), "text": token.text()}))
.collect()
}
fn text_view_token_kind(kind: wip_text_view::TokenKind) -> &'static str {
use wip_text_view::TokenKind;
match kind {
TokenKind::Keyword => "keyword",
TokenKind::TypeName => "type-name",
TokenKind::OperationName => "operation-name",
TokenKind::ParameterName => "parameter-name",
TokenKind::FieldName => "field-name",
TokenKind::CaseName => "case-name",
TokenKind::String => "string",
TokenKind::Reference => "reference",
TokenKind::Documentation => "documentation",
TokenKind::Symbol => "symbol",
TokenKind::Whitespace => "whitespace",
TokenKind::Newline => "newline",
TokenKind::Indentation => "indentation",
_ => "plain",
}
}
fn missing_observation() -> ManualError {
ManualError {
kind: "client",
+106
View File
@@ -166,6 +166,9 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
observed["textView"],
wip_text_view::render_object(&object).unwrap()
);
assert_display_tokens(&observed, &wip_text_view::tokenize_object(&object).unwrap());
let cached = s.manual_observe("/quoted", 0.0).unwrap();
assert_eq!(cached["textViewTokens"], observed["textViewTokens"]);
let text = observed["textView"].as_str().unwrap();
assert!(text.contains("引用 \\\"quote\\\" \\\\ space\\nnext\\tline "));
assert!(text.ends_with("}\n"));
@@ -187,6 +190,17 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
})
.unwrap()
);
assert_display_tokens(
&fetched,
&wip_text_view::tokenize_interface(&wip_text_view::Interface {
reference: &reference,
descriptor,
})
.unwrap(),
);
let cached = s.manual_interface("/", "document").unwrap();
assert_eq!(cached["textViewTokens"], fetched["textViewTokens"]);
assert_eq!(fetched["reference"], reference_json(&reference));
assert_eq!(fetched["descriptor"]["operations"][0]["name"], "read");
assert_eq!(
fetched["descriptor"]["operations"][0]["parameters"],
@@ -194,6 +208,98 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
);
}
fn assert_display_tokens(response: &Json, tokens: &[wip_text_view::Token]) {
let dto = response["textViewTokens"].as_array().unwrap();
assert_eq!(dto.len(), tokens.len());
for (entry, token) in dto.iter().zip(tokens) {
assert_eq!(entry.as_object().unwrap().len(), 2);
assert_eq!(entry["kind"], text_view_token_kind(token.kind()));
assert_eq!(entry["text"], token.text());
}
let concatenated: String = dto
.iter()
.map(|entry| entry["text"].as_str().unwrap())
.collect();
assert_eq!(concatenated, response["textView"].as_str().unwrap());
}
#[test]
fn display_token_kinds_use_the_explicit_kebab_case_whitelist() {
use wip_text_view::TokenKind;
for (kind, expected) in [
(TokenKind::Keyword, "keyword"),
(TokenKind::TypeName, "type-name"),
(TokenKind::OperationName, "operation-name"),
(TokenKind::ParameterName, "parameter-name"),
(TokenKind::FieldName, "field-name"),
(TokenKind::CaseName, "case-name"),
(TokenKind::String, "string"),
(TokenKind::Reference, "reference"),
(TokenKind::Documentation, "documentation"),
(TokenKind::Symbol, "symbol"),
(TokenKind::Whitespace, "whitespace"),
(TokenKind::Newline, "newline"),
(TokenKind::Indentation, "indentation"),
] {
assert_eq!(text_view_token_kind(kind), expected);
}
}
#[test]
fn display_tokens_preserve_quoted_markup_and_unicode_as_raw_text() {
let raw = r#"<script title="引用 🦀">& 'text'</script>"#;
let quoted = r#""<script title=\"引用 🦀\">& 'text'</script>""#;
let mut data: Json = serde_json::from_str(&manifest()).unwrap();
data["nodes"][1]["title"] = json!(raw);
let mut s = BrowserSession::new(&data.to_string()).unwrap();
let observed = s.manual_observe("/quoted", 0.0).unwrap();
let object = s.fresh_object("/quoted").unwrap();
assert_eq!(
observed["textView"],
wip_text_view::render_object(&object).unwrap()
);
assert_display_tokens(&observed, &wip_text_view::tokenize_object(&object).unwrap());
assert!(
observed["textViewTokens"]
.as_array()
.unwrap()
.contains(&json!({"kind": "string", "text": quoted}))
);
s.manual_interface("/", "document").unwrap();
let reference = Worldspace::document_interface();
let mut descriptor = s
.client
.interface(&s.session, &reference)
.unwrap()
.descriptor
.clone()
.unwrap();
descriptor.documentation = Some(Documentation {
summary: raw.into(),
details: None,
});
descriptor.operations[0].name = raw.into();
let interface = wip_text_view::Interface {
reference: &reference,
descriptor: &descriptor,
};
let tokens = wip_text_view::tokenize_interface(&interface).unwrap();
let dto = json!({
"textView": wip_text_view::render_interface(&interface).unwrap(),
"textViewTokens": text_view_tokens(&tokens),
});
// JSON transport must retain literal text, never HTML entities or generated markup.
let decoded: Json = serde_json::from_str(&dto.to_string()).unwrap();
assert_display_tokens(&decoded, &tokens);
let entries = decoded["textViewTokens"].as_array().unwrap();
let quoted_identifier = r#"`<script title=\"引用 🦀\">& 'text'</script>`"#;
assert!(entries.contains(&json!({"kind": "operation-name", "text": quoted_identifier})));
assert!(entries.contains(&json!({"kind": "documentation", "text": format!("/// {quoted}")})));
assert_eq!(descriptor.operations[0].name, raw);
assert_eq!(reference, Worldspace::document_interface());
}
#[test]
fn all_tagged_values_round_trip_without_integer_or_bytes_coercion() {
let values = vec![