Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
55687d3
docs(graphify): add comprehensive knowledge graph analysis
May 6, 2026
70d3433
fix(graphify): clarify PAP transport-agnostic architecture
May 7, 2026
42da230
Merge branch 'docs/graphify-knowledge-graph'
May 14, 2026
ef51c89
docs: add Papillon intent browser UI specification
May 14, 2026
12e71bf
feat(papillon): Add Canvas Tab Bar component
May 14, 2026
11c070a
style(ui): add canvas tab bar CSS
May 14, 2026
86b0eac
feat(papillon): integrate canvas tab bar into topbar
May 14, 2026
6e164a6
feat(papillon-ui): Add workflow_panel_open signal to CanvasState
May 14, 2026
67a2336
feat(papillon): add workflow panel component structure
May 14, 2026
f1ee835
feat(ui): Add workflow panel CSS styles
May 14, 2026
ba61898
feat(papillon): integrate workflow panel into canvas page
May 14, 2026
d5d0248
feat(ui): add workflow chat thread component
May 14, 2026
653cff4
feat(ui): add agent curation list component
May 14, 2026
ff394d8
feat(ui): add disclosure form component
May 14, 2026
1a6426c
feat(papillon): wire workflow panel components together
May 14, 2026
79911ee
feat(papillon): add approval toast notification component
May 14, 2026
e8d619a
feat(papillon): integrate approval toast into canvas page
May 14, 2026
b8a17c1
feat(ui): add ghost block component with skeleton preview
May 14, 2026
52baab3
feat(papillon): add keyboard shortcuts for canvas navigation
May 14, 2026
edd8903
Wire IntentPlan signal from CanvasState to WorkflowPanel (Task 17)
May 14, 2026
c3fc5d0
feat(papillon): wire approval to backend command
May 14, 2026
12ef97c
fix: resolve CSS class mismatches from Task 19 integration testing
May 14, 2026
6ab4b37
chore: update to new Leptos API idioms
May 14, 2026
8e61e6f
fix(ui): add flex layout for canvas-tabs to prevent vertical stacking
May 14, 2026
b506d15
docs: add block-based canvas architecture plan
May 15, 2026
089afcf
feat(shared): add SchemaSignature type for block wiring
May 15, 2026
c5c7e99
test(shared): add missing SchemaSignature test coverage
May 15, 2026
ec2dc8d
feat(shared): add BlockContainer types for visual wiring
May 15, 2026
0a30501
feat(ui): add block input/output port components
May 15, 2026
a8dd60d
feat(ui): add multi-agent selector component
May 15, 2026
3d183c4
feat(ui): add block container component with ports
May 15, 2026
26b7d06
feat(ui): integrate block containers in canvas
May 15, 2026
02f9c29
feat(backend): add create_block_container command
May 15, 2026
64bba9a
feat(backend): add block wiring commands
May 15, 2026
6297efe
Merge branch 'main' of https://github.com/Baur-Software/pap
May 20, 2026
db95d90
feat(assessment): create assessment tool HTML skeleton
May 22, 2026
1e7e9d7
feat(registry-auth): create pluggable auth module with Bearer token v…
May 29, 2026
73095de
feat(registry-auth): extend Config with OIDC, API key, and auth requi…
May 29, 2026
492e581
feat(registry-auth): integrate Bearer token middleware into Axum router
May 29, 2026
06a0902
feat(registry-auth): add optional OIDC and AWS SDK dependencies
May 29, 2026
eb3f425
feat(registry-auth): add Settings page for auth configuration and API…
May 29, 2026
e91f2e5
feat(registry-auth): add auth environment variables to Docker Compose
May 29, 2026
f33455a
feat(registry-terraform): add ECS deployment module for baursoftware-…
May 29, 2026
52728fa
test(registry-auth): verify Bearer token authentication end-to-end
May 29, 2026
baa68fe
test(registry-auth): add integration tests for Bearer token middleware
May 29, 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
1,298 changes: 1,117 additions & 181 deletions Cargo.lock

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions apps/papillon/frontend/src/commands/approval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Signed challenge for identity authorization.
/// Must match the backend SignedChallenge structure.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SignedChallenge {
pub challenge_id: String,
pub signature_b64: String,
}

/// Payload for the approval command.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalPayload {
pub approval_request_id: String,
pub approved: bool,
pub signed_challenge: SignedChallenge,
pub filled_values: Option<HashMap<String, String>>,
pub selected_agent_names: Option<Vec<String>>,
}

/// Call the backend approval command.
///
/// In desktop builds (Tauri), this invokes the `canvas_approve_block` command.
/// In WASM builds, this would call through the bridge (currently returning an error).
pub async fn approve_intent_plan(payload: ApprovalPayload) -> Result<(), String> {
#[cfg(target_family = "wasm")]
{
crate::bridge::invoke("canvas_approve_block", &payload).await
}

#[cfg(not(target_family = "wasm"))]
{
let _ = payload; // Silence unused warning
Err("approve_intent_plan is only available in Tauri builds".to_string())
}
}
1 change: 1 addition & 0 deletions apps/papillon/frontend/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod approval;
102 changes: 102 additions & 0 deletions apps/papillon/frontend/src/components/agent_curation_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use leptos::prelude::*;
use papillon_shared::{AgentCandidate, IntentPlan};

#[component]
pub fn AgentCurationList(
plan: IntentPlan,
selected_agents: RwSignal<Vec<String>>,
) -> impl IntoView {
// Initialize selected agents from plan if not already set
let initial_selected = plan.candidates
.iter()
.filter(|c| Some(&c.did) == plan.selected_agent_did.as_ref())
.map(|c| c.did.clone())
.collect::<Vec<_>>();

if selected_agents.get_untracked().is_empty() && !initial_selected.is_empty() {
selected_agents.set(initial_selected);
}

view! {
<div class="agent-curation-list">
<For
each=move || plan.candidates.clone()
key=|c| c.did.clone()
children=move |candidate| {
view! {
<AgentCurationCard
agent=candidate
selected=selected_agents.read_only()
on_toggle=selected_agents
/>
}
}
/>
</div>
}
}

#[component]
fn AgentCurationCard(
agent: AgentCandidate,
selected: ReadSignal<Vec<String>>,
on_toggle: RwSignal<Vec<String>>,
) -> impl IntoView {
let agent_did = agent.did.clone();
let agent_did_for_toggle = agent.did.clone();

let is_selected = Memo::new(move |_| selected.get().contains(&agent_did));

// On-device detection (simplified: check if did:key)
let is_on_device = agent.did.starts_with("did:key:");

// Truncate DID for display
let truncated_did = truncate_did(&agent.did);

view! {
<div
class="agent-card"
class:selected=is_selected
>
<label class="agent-card-checkbox-label">
<input
type="checkbox"
class="agent-card-checkbox"
checked=is_selected
on:change=move |_| {
on_toggle.update(|list| {
if list.contains(&agent_did_for_toggle) {
list.retain(|d| d != &agent_did_for_toggle);
} else {
list.push(agent_did_for_toggle.clone());
}
});
}
/>
<div class="agent-card-info">
<div class="agent-card-header">
<span class="agent-card-name">{agent.name.clone()}</span>
<Show when=move || is_on_device>
<span class="trust-badge on-device">"On-Device"</span>
</Show>
</div>
<span class="agent-card-did">{truncated_did}</span>
<div class="agent-card-disclosure">
<span class="disclosure-count">
{agent.requires_disclosure.len()}
" "
{if agent.requires_disclosure.len() == 1 { "property" } else { "properties" }}
</span>
</div>
</div>
</label>
</div>
}
}

fn truncate_did(did: &str) -> String {
if did.len() <= 30 {
return did.to_string();
}
format!("{}...{}", &did[..20], &did[did.len() - 8..])
}
55 changes: 55 additions & 0 deletions apps/papillon/frontend/src/components/agent_selector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use leptos::prelude::*;
use papillon_shared::AgentInfo;

/// Multi-select agent picker for a block container.
/// Shows only agents matching the container's SchemaSignature.
#[component]
pub fn AgentSelector(
/// List of compatible agents (filtered by signature)
agents: Vec<AgentInfo>,
/// Currently selected agent names
selected: RwSignal<Vec<String>>,
) -> impl IntoView {
let toggle_agent = move |name: String| {
selected.update(|sel| {
if sel.contains(&name) {
sel.retain(|n| n != &name);
} else {
sel.push(name);
}
});
};

view! {
<div class="agent-selector">
<div class="agent-selector-header">"Select Agents"</div>
<div class="agent-selector-list">
<For
each=move || agents.clone()
key=|agent| agent.name.clone()
children=move |agent| {
let agent_name_for_click = agent.name.clone();
let agent_name_for_check = agent.name.clone();
let agent_name_for_class = agent.name.clone();

view! {
<button
class="agent-selector-item"
class:selected=move || selected.get().contains(&agent_name_for_class)
on:click=move |_| toggle_agent(agent_name_for_click.clone())
>
<div class="agent-checkbox">
{move || if selected.get().contains(&agent_name_for_check) { "✓" } else { "" }}
</div>
<div class="agent-info">
<div class="agent-name">{agent.name.clone()}</div>
<div class="agent-provider">{agent.provider_name.clone()}</div>
</div>
</button>
}
}
/>
</div>
</div>
}
}
162 changes: 162 additions & 0 deletions apps/papillon/frontend/src/components/approval_toast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use leptos::prelude::*;
use crate::state::canvas::CanvasState;

/// Toast notification stack for approval requests.
/// Fixed bottom-right position, animates in from bottom.
#[component]
pub fn ApprovalToastStack() -> impl IntoView {
let canvas_state = expect_context::<CanvasState>();
let hitl_pending = canvas_state.hitl_pending;

view! {
<Show when=move || hitl_pending.get().is_some()>
{move || {
hitl_pending.get().map(|req| {
view! {
<ApprovalToast request=req />
}
})
}}
</Show>
}
}

/// Individual approval toast for a single HitL request.
#[component]
fn ApprovalToast(request: crate::state::canvas::HitlRequest) -> impl IntoView {
let canvas_state = expect_context::<CanvasState>();

// Humanize action type: strip "schema:" prefix and "Action" suffix
let action_display = humanize_action(&request.action_type);

// Clone fields needed in closures
let agent_name = request.agent_name.clone();
let disclosure_props_len = request.disclosure_props.len();
let has_disclosure = !request.disclosure_props.is_empty();

// Quick approve handler
let quick_approve = move |_| {
if disclosure_props_len == 0 {
// Zero-disclosure: approve immediately
// TODO: wire to canvas_state.approve_block() in Task 18
leptos::logging::log!("Quick approve: {}", agent_name);
canvas_state.hitl_pending.set(None);
} else {
// Has disclosure: open workflow panel for review
canvas_state.workflow_panel_open.set(true);
}
};

// Dismiss handler
let dismiss = move |_| {
canvas_state.hitl_pending.set(None);
};

view! {
<div class="approval-toast">
<div class="approval-toast-header">
<div class="approval-toast-agent">{request.agent_name.clone()}</div>
<button
class="approval-toast-dismiss"
on:click=dismiss
aria-label="Dismiss"
>
"×"
</button>
</div>

<div class="approval-toast-body">
<div class="approval-toast-action">
{action_display}
{move || {
if request.risk_level == "HIGH" {
view! { <span class="approval-toast-risk high">"HIGH"</span> }.into_any()
} else if request.risk_level == "CRITICAL" {
view! { <span class="approval-toast-risk critical">"CRITICAL"</span> }.into_any()
} else {
view! { <></> }.into_any()
}
}}
</div>

<div class="approval-toast-description">
{request.description.clone()}
</div>

{move || {
if !request.disclosure_props.is_empty() {
view! {
<div class="approval-toast-disclosure">
"Requires disclosure: "
{request.disclosure_props.join(", ")}
</div>
}.into_any()
} else {
view! { <></> }.into_any()
}
}}
</div>

<div class="approval-toast-footer">
{
if has_disclosure {
// Multi-property disclosure: view details button
view! {
<button
class="approval-toast-button details"
on:click=quick_approve
>
"VIEW DETAILS"
</button>
}.into_any()
} else {
// Zero-disclosure: quick approve button
view! {
<button
class="approval-toast-button approve"
on:click=quick_approve
>
"APPROVE"
</button>
}.into_any()
}
}
</div>
</div>
}
}

/// Strip "schema:" prefix and "Action" suffix from action type strings.
fn humanize_action(action: &str) -> String {
action
.trim_start_matches("schema:")
.trim_end_matches("Action")
.to_string()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn humanize_action_strips_prefix_and_suffix() {
assert_eq!(humanize_action("schema:SearchAction"), "Search");
assert_eq!(humanize_action("schema:WriteAction"), "Write");
assert_eq!(humanize_action("schema:ReadAction"), "Read");
}

#[test]
fn humanize_action_handles_no_prefix() {
assert_eq!(humanize_action("SearchAction"), "Search");
}

#[test]
fn humanize_action_handles_no_suffix() {
assert_eq!(humanize_action("schema:Search"), "schema:Search");
}

#[test]
fn humanize_action_handles_plain_string() {
assert_eq!(humanize_action("search"), "search");
}
}
Loading
Loading