Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/ironclaw_common/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,19 @@ pub enum AppEvent {
},

/// Skills activated for a conversation turn.
///
/// `feedback` is a list of human-readable notes about the
/// activation (e.g. "chain-loaded from code-review", "ceo-setup
/// excluded by setup marker"). May be empty — `skip_serializing_if`
/// keeps the SSE payload lean for the common no-note case and
/// preserves wire-format backwards compatibility.
#[serde(rename = "skill_activated")]
SkillActivated {
skill_names: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
feedback: Vec<String>,
},

/// Extension activation status change (WASM channels).
Expand Down Expand Up @@ -523,6 +531,7 @@ mod tests {
AppEvent::SkillActivated {
skill_names: vec![],
thread_id: None,
feedback: vec![],
},
AppEvent::ExtensionStatus {
extension_name: ExtensionName::from_trusted(String::new()),
Expand Down
49 changes: 49 additions & 0 deletions crates/ironclaw_gateway/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,15 @@ function connectSSE(lastEventIdOverride) {
}
});

addTrackedEventListener('skill_activated', (e) => {
const data = JSON.parse(e.data);
if (!isCurrentThread(data.thread_id)) return;
const names = Array.isArray(data.skill_names) ? data.skill_names : [];
const feedback = Array.isArray(data.feedback) ? data.feedback : [];
if (names.length === 0 && feedback.length === 0) return;
addSkillActivationCard(names, feedback);
});

addTrackedEventListener('tool_started', (e) => {
const data = JSON.parse(e.data);
if (data.thread_id) {
Expand Down Expand Up @@ -2779,6 +2788,46 @@ function applyToolActivityCardState(rendered, options) {
setToolActivityCardExpanded(rendered, shouldAutoExpand);
}

function addSkillActivationCard(names, feedback) {
// The activation card is the first concrete signal that a turn is
// underway, so the "thinking" dots have done their job — drop them.
// `startTool` / `completeTool` will re-show them later if nothing
// else has reported progress by then.
removeActivityThinking();
const group = getOrCreateActivityGroup();
if (!group) return;

const card = document.createElement('div');
card.className = 'activity-skill-card';

const header = document.createElement('div');
header.className = 'activity-skill-header';
const icon = document.createElement('span');
icon.className = 'activity-skill-icon';
icon.textContent = '\u25C6'; // ◆
header.appendChild(icon);
const label = document.createElement('span');
label.className = 'activity-skill-label';
if (names.length > 0) {
label.textContent = 'Skills: ' + names.join(', ');
} else {
label.textContent = 'Skill activation';
}
header.appendChild(label);
card.appendChild(header);

for (const note of feedback) {
const row = document.createElement('div');
row.className = 'activity-skill-note';
row.textContent = note;
card.appendChild(row);
}

group.appendChild(card);
const container = document.getElementById('chat-messages');
container.scrollTop = container.scrollHeight;
}

function createToolActivityCard(entry, options) {
const card = document.createElement('div');
card.className = 'activity-tool-card';
Expand Down
36 changes: 36 additions & 0 deletions crates/ironclaw_gateway/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,42 @@ body {
color: var(--text-secondary);
}

.activity-skill-card {
display: flex;
flex-direction: column;
gap: 3px;
padding: 6px 10px;
border-left: 2px solid var(--accent);
background: var(--surface-muted, rgba(127, 127, 127, 0.06));
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 4px 0;
border-radius: 3px;
}

.activity-skill-header {
display: flex;
align-items: center;
gap: 6px;
}

.activity-skill-icon {
color: var(--accent);
font-size: 0.85em;
}

.activity-skill-label {
font-weight: 500;
color: var(--text-primary);
}

.activity-skill-note {
padding-left: 18px;
font-size: 0.9em;
color: var(--text-secondary);
font-style: italic;
}

.activity-thinking-dots {
display: flex;
gap: 3px;
Expand Down
4 changes: 3 additions & 1 deletion crates/ironclaw_skills/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ pub use types::{

pub use gating::{GatingResult, check_requirements, check_requirements_sync};
pub use parser::{ParsedSkill, SkillParseError, parse_skill_md};
pub use selector::{MAX_SKILL_CONTEXT_TOKENS, extract_skill_mentions, prefilter_skills};
pub use selector::{
MAX_SKILL_CONTEXT_TOKENS, SelectionOutcome, extract_skill_mentions, prefilter_skills,
};
pub use validation::{
escape_skill_content, escape_xml_attr, normalize_line_endings, validate_credential_name,
validate_credential_spec, validate_skill_name,
Expand Down
Loading
Loading