Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f46c60a
feat(workflows): add URL-addressable editor foundation
tellaho Aug 18, 2026
37865d4
fix(workflows): restore final editor interactions
tellaho Aug 18, 2026
4508bc7
fix(workflows): restore editor shell parity
tellaho Aug 18, 2026
503c09f
fix(workflows): preserve reaction trigger filters in form mode
tellaho Aug 18, 2026
756c572
fix(workflows): gate creation inspector on channel selection
tellaho Aug 18, 2026
76a8c2a
test(workflows): align navigation coverage with editor flow
tellaho Aug 18, 2026
4e0c58c
fix(workflows): protect editor lifecycle states
tellaho Aug 18, 2026
10a74c8
Merge origin/main into tho/workflow-editor-foundation
tellaho Aug 18, 2026
34454d9
fix(workflows): add run history inspector
tellaho Aug 18, 2026
3d27dfc
fix(workflows): preserve honest detail actions
tellaho Aug 18, 2026
11183c9
test(workflows): disambiguate detail trigger assertion
tellaho Aug 18, 2026
9c496d7
fix(workflows): unify detail and edit views
tellaho Aug 18, 2026
c9c1d35
fix(workflows): simplify card borders
tellaho Aug 18, 2026
5d6468f
fix(workflows): animate default trigger pane
tellaho Aug 18, 2026
244da3e
test(workflows): cover unified workflow modal
tellaho Aug 19, 2026
b59375b
Merge origin/main into tho/workflow-editor-foundation
tellaho Aug 19, 2026
967283c
feat(desktop): add workflow schedule and text controls
tellaho Aug 19, 2026
2a0cf5f
fix(workflows): hide unsupported run history
tellaho Aug 19, 2026
7afc9d0
fix(workflows): align message filter controls
tellaho Aug 19, 2026
1fc9802
feat(workflows): align step settings with prototype
tellaho Aug 19, 2026
3903120
fix(workflows): scope structured step conditions
tellaho Aug 19, 2026
bba68c6
feat(workflows): add duration timeout control
tellaho Aug 19, 2026
820cc1a
feat(workflows): add channel settings ingress
tellaho Aug 19, 2026
d779021
feat(workflows): open channel workflows over the channel
tellaho Aug 19, 2026
da9769c
fix(workflows): close the editor when its workflow is deleted
tellaho Aug 19, 2026
a95b9bd
Merge remote-tracking branch 'origin/main' into tho/workflow-editor-f…
tellaho Aug 19, 2026
c5c3abc
fix(workflows): keep the editor title stable across panes
tellaho Aug 19, 2026
cabf4c6
fix(workflows): show executable trigger guidance
tellaho Aug 19, 2026
7f6189a
Merge remote-tracking branch 'origin/main' into tho/workflow-editor-f…
tellaho Aug 19, 2026
0682e87
fix(workflows): confirm dirty editor transitions
tellaho Aug 19, 2026
3760c3d
fix(workflows): preserve drafts when deletion fails
tellaho Aug 19, 2026
aeed6d4
fix(workflows): gate channel settings ingress
tellaho Aug 20, 2026
59bee36
Merge remote-tracking branch 'origin/main' into tho/workflow-editor-f…
tellaho Aug 20, 2026
dc28ffa
fix(workflows): pick reactions in the Reaction Added trigger
tellaho Aug 20, 2026
76ebba7
fix(workflows): return channel editors to workflows panel
tellaho Aug 20, 2026
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
92 changes: 60 additions & 32 deletions crates/buzz-workflow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ async fn should_fire_workflow(
) -> bool {
if let TriggerDef::ReactionAdded {
emoji: Some(ref expected),
..
} = def.trigger
{
if &trigger_ctx.emoji != expected {
Expand All @@ -900,33 +901,13 @@ async fn should_fire_workflow(
}
}

if let TriggerDef::MessagePosted {
filter: Some(ref expr),
} = def.trigger
{
match executor::evaluate_condition(expr, trigger_ctx, &HashMap::new()).await {
Ok(true) => {}
Ok(false) => {
tracing::debug!(
workflow_id = %workflow_id,
"Trigger filter evaluated false — skipping workflow"
);
return false;
}
Err(e) => {
tracing::warn!(
workflow_id = %workflow_id,
"Trigger filter error: {e} — skipping workflow"
);
return false;
}
}
}

if let TriggerDef::DiffPosted {
filter: Some(ref expr),
} = def.trigger
{
let filter = match &def.trigger {
TriggerDef::MessagePosted { filter }
| TriggerDef::ReactionAdded { filter, .. }
| TriggerDef::DiffPosted { filter } => filter.as_ref(),
TriggerDef::Schedule { .. } | TriggerDef::Webhook => None,
};
if let Some(expr) = filter {
match executor::evaluate_condition(expr, trigger_ctx, &HashMap::new()).await {
Ok(true) => {}
Ok(false) => {
Expand Down Expand Up @@ -1364,7 +1345,10 @@ steps:

#[test]
fn trigger_matches_reaction() {
let trigger = TriggerDef::ReactionAdded { emoji: None };
let trigger = TriggerDef::ReactionAdded {
emoji: None,
filter: None,
};
assert!(trigger_matches_event(
&trigger,
buzz_core::kind::KIND_REACTION
Expand All @@ -1375,6 +1359,36 @@ steps:
));
}

#[tokio::test]
async fn reaction_filter_matches_target_message() {
let yaml = r#"
name: "React to one message"
trigger:
on: reaction_added
filter: 'trigger_message_id == "target-message"'
steps:
- id: wait
action: delay
duration: 1s
"#;
let (def, _) = WorkflowEngine::parse_yaml(yaml).expect("parse failed");
let mut trigger_ctx = executor::TriggerContext {
message_id: "target-message".to_owned(),
..Default::default()
};

assert!(
should_fire_workflow(&def, &trigger_ctx, Uuid::new_v4()).await,
"reaction to the selected message should fire"
);

trigger_ctx.message_id = "different-message".to_owned();
assert!(
!should_fire_workflow(&def, &trigger_ctx, Uuid::new_v4()).await,
"reaction to a different message should be filtered out"
);
}

#[test]
fn schedule_trigger_never_matches_events() {
let trigger = TriggerDef::Schedule {
Expand Down Expand Up @@ -1421,7 +1435,10 @@ steps:

#[test]
fn reaction_added_matches_kind_7_only() {
let trigger = TriggerDef::ReactionAdded { emoji: None };
let trigger = TriggerDef::ReactionAdded {
emoji: None,
filter: None,
};
// Must match KIND_REACTION = 7.
assert!(trigger_matches_event(&trigger, 7));
// Must NOT match stream message (kind 9).
Expand All @@ -1436,6 +1453,7 @@ steps:
// trigger_matches_event only checks the kind number.
let trigger = TriggerDef::ReactionAdded {
emoji: Some("thumbsup".to_owned()),
filter: None,
};
assert!(trigger_matches_event(&trigger, 7));
assert!(!trigger_matches_event(&trigger, 9));
Expand All @@ -1458,7 +1476,10 @@ steps:
// before calling trigger_matches_event, but verify the function itself
// also returns false for these kinds.
let msg_trigger = TriggerDef::MessagePosted { filter: None };
let react_trigger = TriggerDef::ReactionAdded { emoji: None };
let react_trigger = TriggerDef::ReactionAdded {
emoji: None,
filter: None,
};

for kind in buzz_core::kind::KIND_WORKFLOW_TRIGGERED
..=buzz_core::kind::KIND_WORKFLOW_APPROVAL_DENIED
Expand All @@ -1478,7 +1499,10 @@ steps:
fn trigger_matches_event_kind_zero_matches_nothing() {
// Kind 0 is a profile event — no trigger should match it.
let msg_trigger = TriggerDef::MessagePosted { filter: None };
let react_trigger = TriggerDef::ReactionAdded { emoji: None };
let react_trigger = TriggerDef::ReactionAdded {
emoji: None,
filter: None,
};
let sched_trigger = TriggerDef::Schedule {
cron: None,
interval: Some("1h".to_owned()),
Expand Down Expand Up @@ -1715,7 +1739,11 @@ steps:
async fn setup_db() -> buzz_db::Db {
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| "postgres://buzz:buzz_dev@localhost:5432/buzz".to_owned());
// Local-only test default; this is not a production credential.
.unwrap_or_else(|_| {
let local_test_database = "postgres://buzz:buzz_dev@localhost:5432/buzz"; // sadscan:disable np.postgres.1
local_test_database.to_owned()
});
buzz_db::Db::new(&buzz_db::DbConfig {
database_url,
..Default::default()
Expand Down
11 changes: 8 additions & 3 deletions crates/buzz-workflow/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub enum TriggerDef {
/// Optional: only fire for this specific emoji.
#[serde(default)]
emoji: Option<String>,
/// Optional evalexpr filter over the reaction context.
#[serde(default)]
filter: Option<String>,
},
/// Fires when a diff message (kind:40008) is posted in the workflow's channel.
DiffPosted {
Expand Down Expand Up @@ -300,11 +303,12 @@ mod tests {

#[test]
fn parse_reaction_added_trigger() {
let yaml = "name: Triage\ntrigger:\n on: reaction_added\n emoji: clipboard\nsteps:\n - id: ack\n action: add_reaction\n emoji: eyes\n";
let yaml = "name: Triage\ntrigger:\n on: reaction_added\n emoji: clipboard\n filter: 'trigger_message_id == \"abc123\"'\nsteps:\n - id: ack\n action: add_reaction\n emoji: eyes\n";
let (def, _) = parse_yaml(yaml).expect("parse failed");
match &def.trigger {
TriggerDef::ReactionAdded { emoji } => {
TriggerDef::ReactionAdded { emoji, filter } => {
assert_eq!(emoji.as_deref(), Some("clipboard"));
assert_eq!(filter.as_deref(), Some("trigger_message_id == \"abc123\""));
}
other => panic!("unexpected trigger: {other:?}"),
}
Expand Down Expand Up @@ -488,8 +492,9 @@ mod tests {
let yaml = "name: Any Reaction\ntrigger:\n on: reaction_added\nsteps:\n - id: s1\n action: add_reaction\n emoji: eyes\n";
let (def, _) = parse_yaml(yaml).expect("parse failed");
match &def.trigger {
TriggerDef::ReactionAdded { emoji } => {
TriggerDef::ReactionAdded { emoji, filter } => {
assert!(emoji.is_none(), "emoji should default to None");
assert!(filter.is_none(), "filter should default to None");
}
other => panic!("unexpected trigger: {other:?}"),
}
Expand Down
3 changes: 3 additions & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export default defineConfig({
"**/relay-reconnect.spec.ts",
"**/relay-reconnect-affordance.spec.ts",
"**/workflows.spec.ts",
"**/workflow-reaction-picker.spec.ts",
"**/workflow-local-controls.spec.ts",
"**/workflow-title-stability.spec.ts",
"**/identity-archive.spec.ts",
"**/identity-archive-hide.spec.ts",
"**/relay-connectivity.spec.ts",
Expand Down
Loading
Loading