use std::cell::RefCell;
thread_local! {
static CAPTURED_STDERR: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn start_stderr_capture() {
CAPTURED_STDERR.with(|cell| {
*cell.borrow_mut() = Some(String::new());
});
}
pub fn take_captured_stderr() -> Option<String> {
CAPTURED_STDERR.with(|cell| cell.borrow_mut().take())
}
fn write_stderr(message: &str) {
let mut captured = false;
CAPTURED_STDERR.with(|cell| {
if let Some(ref mut buffer) = *cell.borrow_mut() {
buffer.push_str(message);
buffer.push('\n');
captured = true;
}
});
if !captured {
eprintln!("{}", message);
}
}
#[derive(Debug, Clone, Default)]
pub struct TextQualityResult {
pub text: String,
pub warnings: Vec<String>,
pub suggestions: Vec<String>,
pub escape_sequences_repaired: bool,
}
pub fn repair_escape_sequences(text: &str) -> (String, bool) {
if text.contains("\\n") {
(text.replace("\\n", "\n"), true)
} else {
(text.to_string(), false)
}
}
pub fn has_markdown_formatting(text: &str) -> bool {
let markdown_patterns: &[fn(&str) -> bool] = &[
|t| {
t.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("# ")
|| trimmed.starts_with("## ")
|| trimmed.starts_with("### ")
|| trimmed.starts_with("#### ")
|| trimmed.starts_with("##### ")
|| trimmed.starts_with("###### ")
})
},
|t| contains_pattern(t, "**"),
|t| contains_inline_emphasis(t, '*'),
|t| contains_pattern(t, "__"),
|t| contains_inline_emphasis(t, '_'),
|t| t.contains("```"),
|t| t.contains('`'),
|t| t.lines().any(|line| line.trim_start().starts_with('>')),
|t| {
t.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ")
})
},
|t| {
t.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.len() > 2
&& trimmed
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_digit())
&& trimmed.contains(". ")
})
},
|t| t.lines().any(|line| line.trim() == "---"),
|t| t.contains("]("),
];
markdown_patterns.iter().any(|check| check(text))
}
fn contains_pattern(text: &str, pattern: &str) -> bool {
let count = text.matches(pattern).count();
count >= 2
}
fn contains_inline_emphasis(text: &str, marker: char) -> bool {
let mut chars = text.chars().peekable();
let mut in_emphasis = false;
let mut prev = ' ';
while let Some(ch) = chars.next() {
if ch == marker && prev != marker {
if let Some(&next) = chars.peek() {
if in_emphasis && !next.is_whitespace() {
return true;
}
if !in_emphasis && !next.is_whitespace() && next != marker {
in_emphasis = true;
} else if in_emphasis {
in_emphasis = false;
}
}
}
prev = ch;
}
false
}
pub fn has_diagram_block(text: &str) -> bool {
text.lines().any(|line| {
let trimmed = line.trim();
trimmed.eq_ignore_ascii_case("```mermaid")
|| trimmed.eq_ignore_ascii_case("```plantuml")
|| trimmed.eq_ignore_ascii_case("```d2")
})
}
pub fn apply_text_quality_signals(text: &str) -> TextQualityResult {
let (repaired_text, sequences_were_repaired) = repair_escape_sequences(text);
let mut warnings = Vec::new();
let mut suggestions = Vec::new();
if sequences_were_repaired {
warnings.push(
"WARNING: Literal \\n escape sequences were detected and replaced with real newlines.\n\
\x20 To pass multi-line text correctly, use a heredoc or $'...\\n...' syntax:\n\
\x20 kbs create \"Title\" --description $'First line\\nSecond line'\n\
\x20 kbs create \"Title\" --description \"$(cat <<'EOF'\\nFirst line\\nSecond line\\nEOF\\n)\""
.to_string(),
);
}
if !has_markdown_formatting(&repaired_text) {
suggestions.push(
"SUGGESTION: Markdown formatting is supported in descriptions and comments.\n\
\x20 Use headings, bold, code blocks, and lists when they improve readability."
.to_string(),
);
}
if !has_diagram_block(&repaired_text) {
suggestions.push(
"SUGGESTION: Diagrams can be embedded using fenced code blocks.\n\
\x20 Supported: mermaid, plantuml, d2. Use these to visualize flows or structures."
.to_string(),
);
}
TextQualityResult {
text: repaired_text,
warnings,
suggestions,
escape_sequences_repaired: sequences_were_repaired,
}
}
pub fn emit_signals(
result: &TextQualityResult,
context: &str,
issue_id: Option<&str>,
comment_id: Option<&str>,
_is_update: bool,
) {
for warning in &result.warnings {
write_stderr(warning);
}
if !result.suggestions.is_empty() {
for suggestion in &result.suggestions {
write_stderr(suggestion);
}
if let Some(issue_id) = issue_id {
emit_follow_up_hint(context, issue_id, comment_id);
}
}
}
fn emit_follow_up_hint(context: &str, issue_id: &str, comment_id: Option<&str>) {
if let Some(comment_id) = comment_id {
write_stderr(&format!(
" -> To update this comment: kbs comment update {} {} \"<your improved comment here>\"",
issue_id, comment_id
));
} else {
write_stderr(&format!(
" -> To update the {}: kbs update {} --description \"<your improved description here>\"",
context, issue_id
));
}
}