Better handling skill depdenencies on ENV VAR. (#9017)

An experimental flow for env var skill dependencies. Skills can now
declare required env vars in SKILL.md; if missing, the CLI prompts the
user to get the value, and Core will store it in memory (eventually to a
local persistent store)
<img width="790" height="169" alt="image"
src="https://github.com/user-attachments/assets/cd928918-9403-43cb-a7e7-b8d59bcccd9a"
/>
This commit is contained in:
xl-openai
2026-01-29 11:13:30 -08:00
committed by GitHub
parent b7f26d74f0
commit bdd8a7d58b
15 changed files with 289 additions and 2 deletions

View File

@@ -2854,6 +2854,12 @@ impl Renderable for ChatComposer {
}
fn render(&self, area: Rect, buf: &mut Buffer) {
self.render_with_mask(area, buf, None);
}
}
impl ChatComposer {
pub(crate) fn render_with_mask(&self, area: Rect, buf: &mut Buffer, mask_char: Option<char>) {
let [composer_rect, textarea_rect, popup_rect] = self.layout_areas(area);
match &self.active_popup {
ActivePopup::Command(popup) => {
@@ -3018,7 +3024,12 @@ impl Renderable for ChatComposer {
}
let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
if let Some(mask_char) = mask_char {
self.textarea
.render_ref_masked(textarea_rect, buf, &mut state, mask_char);
} else {
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
}
if self.textarea.text().is_empty() {
let text = if self.input_enabled {
self.placeholder_text.as_str().to_string()

View File

@@ -1251,6 +1251,7 @@ mod tests {
header: header.to_string(),
question: "Choose an option.".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
label: "Option 1".to_string(),
@@ -1274,6 +1275,7 @@ mod tests {
header: header.to_string(),
question: "Choose an option.".to_string(),
is_other: true,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
label: "Option 1".to_string(),
@@ -1297,6 +1299,7 @@ mod tests {
header: header.to_string(),
question: "Choose the next step for this task.".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
label: "Discuss a code change".to_string(),
@@ -1326,6 +1329,7 @@ mod tests {
header: header.to_string(),
question: "Share details.".to_string(),
is_other: false,
is_secret: false,
options: None,
}
}
@@ -2385,6 +2389,7 @@ mod tests {
header: "Next Step".to_string(),
question: "What would you like to do next?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
label: "Discuss a code change (Recommended)".to_string(),
@@ -2436,6 +2441,7 @@ mod tests {
header: "Next Step".to_string(),
question: "What would you like to do next?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![
RequestUserInputQuestionOption {
label: "Discuss a code change (Recommended)".to_string(),

View File

@@ -414,7 +414,14 @@ impl RequestUserInputOverlay {
if area.width == 0 || area.height == 0 {
return;
}
self.composer.render(area, buf);
let is_secret = self
.current_question()
.is_some_and(|question| question.is_secret);
if is_secret {
self.composer.render_with_mask(area, buf, Some('*'));
} else {
self.composer.render(area, buf);
}
}
}

View File

@@ -1146,6 +1146,22 @@ impl StatefulWidgetRef for &TextArea {
}
impl TextArea {
pub(crate) fn render_ref_masked(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut TextAreaState,
mask_char: char,
) {
let lines = self.wrapped_lines(area.width);
let scroll = self.effective_scroll(area.height, &lines, state.scroll);
state.scroll = scroll;
let start = scroll as usize;
let end = (scroll + area.height).min(lines.len() as u16) as usize;
self.render_lines_masked(area, buf, &lines, start..end, mask_char);
}
fn render_lines(
&self,
area: Rect,
@@ -1175,6 +1191,26 @@ impl TextArea {
}
}
}
fn render_lines_masked(
&self,
area: Rect,
buf: &mut Buffer,
lines: &[Range<usize>],
range: std::ops::Range<usize>,
mask_char: char,
) {
for (row, idx) in range.enumerate() {
let r = &lines[idx];
let y = area.y + row as u16;
let line_range = r.start..r.end - 1;
let masked = self.text[line_range.clone()]
.chars()
.map(|_| mask_char)
.collect::<String>();
buf.set_string(area.x, y, &masked, Style::default());
}
}
}
#[cfg(test)]