Restore native communication shell grammar
This commit is contained in:
1 parent
95c539380b
commit
ac9be2b639
3 files changed
+447
-292
No files matched your search
@@ -7,227 +7,36 @@ use super::{
|
||||
};
|
||||
|
||||
impl AppRoot {
|
||||
pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
|
||||
pub(super) fn render_messages<'a>(
|
||||
&'a self,
|
||||
cx: &'a mut Context<Self>,
|
||||
) -> impl IntoElement + 'a {
|
||||
let selected = self.state.selected_message_id.as_ref().and_then(|id| {
|
||||
self.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.find(|item| &item.id == id)
|
||||
});
|
||||
let company_wide = self
|
||||
.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| item.channel == "company")
|
||||
.collect::<Vec<_>>();
|
||||
let department = self
|
||||
.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| item.channel == "department")
|
||||
.collect::<Vec<_>>();
|
||||
let direct = self
|
||||
.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| item.channel == "direct")
|
||||
.collect::<Vec<_>>();
|
||||
let task_threads = self
|
||||
.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.channel != "company"
|
||||
&& item.channel != "department"
|
||||
&& item.channel != "direct"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let channel_groups = message_channel_groups(&self.snapshot);
|
||||
let selected_channel = selected.map(message_channel_key).or_else(|| {
|
||||
channel_groups
|
||||
.first()
|
||||
.and_then(|group| group.rows.first().map(|row| row.key.clone()))
|
||||
});
|
||||
let visible_messages = selected_channel
|
||||
.as_ref()
|
||||
.map(|channel_key| messages_for_channel(&self.snapshot, channel_key))
|
||||
.unwrap_or_default();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.w(px(356.0))
|
||||
.p_5()
|
||||
.gap_4()
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(message_section_label("COMPANY-WIDE"))
|
||||
.children(company_wide.iter().map(|message| {
|
||||
let active =
|
||||
self.state.selected_message_id.as_deref() == Some(message.id.as_str());
|
||||
let message_id = message.id.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("message-{}", message.id)))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.cursor_pointer()
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_message_id = Some(message_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(if active {
|
||||
theme::sidebar_text_active()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.truncate()
|
||||
.child(message.subject.clone()),
|
||||
)
|
||||
.when(message.priority == "urgent", |container| {
|
||||
container.child(
|
||||
div()
|
||||
.w(px(10.0))
|
||||
.h(px(10.0))
|
||||
.rounded_full()
|
||||
.bg(theme::danger()),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.child(message_section_label("DEPARTMENT"))
|
||||
.children(department.iter().map(|message| {
|
||||
let active =
|
||||
self.state.selected_message_id.as_deref() == Some(message.id.as_str());
|
||||
let message_id = message.id.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("message-{}", message.id)))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.cursor_pointer()
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_message_id = Some(message_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(if active {
|
||||
theme::sidebar_text_active()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.truncate()
|
||||
.child(message.subject.clone()),
|
||||
)
|
||||
}))
|
||||
.child(message_section_label("DIRECT MESSAGES"))
|
||||
.children(direct.iter().map(|message| {
|
||||
let active =
|
||||
self.state.selected_message_id.as_deref() == Some(message.id.as_str());
|
||||
let message_id = message.id.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("message-{}", message.id)))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.cursor_pointer()
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_message_id = Some(message_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(if active {
|
||||
theme::sidebar_text_active()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.truncate()
|
||||
.child(message.subject.clone()),
|
||||
)
|
||||
.when(message.read_at.is_none(), |container| {
|
||||
container.child(
|
||||
div()
|
||||
.w(px(10.0))
|
||||
.h(px(10.0))
|
||||
.rounded_full()
|
||||
.bg(theme::danger()),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.child(message_section_label("TASK DISCUSSIONS"))
|
||||
.children(task_threads.iter().map(|message| {
|
||||
let active =
|
||||
self.state.selected_message_id.as_deref() == Some(message.id.as_str());
|
||||
let message_id = message.id.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("message-{}", message.id)))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.cursor_pointer()
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_message_id = Some(message_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(if active {
|
||||
theme::sidebar_text_active()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.truncate()
|
||||
.child(message.subject.clone()),
|
||||
)
|
||||
}))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.text_xs()
|
||||
.text_color(theme::text_muted())
|
||||
.child(format!("{} channels", self.snapshot.agent_messages.len()))
|
||||
.child(format!("{} agents", self.snapshot.agents.len())),
|
||||
),
|
||||
)
|
||||
.child(render_message_sidebar(
|
||||
&channel_groups,
|
||||
selected_channel.as_deref(),
|
||||
&self.state.selected_message_id,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -235,92 +44,438 @@ impl AppRoot {
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_5()
|
||||
.py_4()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_border())
|
||||
.child(
|
||||
div().text_lg().text_color(theme::text_primary()).child(
|
||||
selected
|
||||
.map(|message| message.subject.clone())
|
||||
.unwrap_or_else(|| "Select a conversation".to_string()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(search_shell("Search messages"))
|
||||
.child(filter_shell()),
|
||||
),
|
||||
)
|
||||
.child(render_message_header(
|
||||
selected_channel.as_deref(),
|
||||
visible_messages.len(),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.id("messages-stream")
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.overflow_scroll()
|
||||
.child(selected_message_card(selected))
|
||||
.when_some(secondary_message(selected), |container, message| {
|
||||
container.child(compact_message_card(message))
|
||||
.when(visible_messages.is_empty(), |container| {
|
||||
container.child(selected_message_card(None))
|
||||
})
|
||||
.when(!visible_messages.is_empty(), |container| {
|
||||
container.children(visible_messages.iter().map(|message| {
|
||||
let is_selected = self.state.selected_message_id.as_deref()
|
||||
== Some(message.id.as_str());
|
||||
let message_id = message.id.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"message-stream-{}",
|
||||
message.id
|
||||
)))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_message_id =
|
||||
Some(message_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(if is_selected {
|
||||
selected_message_card(Some(message)).into_any_element()
|
||||
} else {
|
||||
compact_message_card(message).into_any_element()
|
||||
})
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.border_t_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(composer_chip(selected_recipient(selected).as_str()))
|
||||
.child(composer_subject(selected)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(12.0))
|
||||
.p_3()
|
||||
.rounded_lg()
|
||||
.bg(theme::panel_inset())
|
||||
.border_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.child(self.message_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(theme::text_muted())
|
||||
.child("Press Cmd+Enter to send"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(primary_button("Send update", cx, |this, cx| {
|
||||
this.send_message(cx)
|
||||
}))
|
||||
.child(secondary_button(
|
||||
"Mark read",
|
||||
cx,
|
||||
|this, cx| this.mark_selected_message_read(cx),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
.child(render_message_composer(self, selected, cx)),
|
||||
)
|
||||
.child(render_message_inspector(selected))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ChannelRow {
|
||||
key: String,
|
||||
label: String,
|
||||
count: usize,
|
||||
urgent: bool,
|
||||
rows: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ChannelGroup {
|
||||
label: &'static str,
|
||||
rows: Vec<ChannelRow>,
|
||||
}
|
||||
|
||||
fn message_channel_groups(snapshot: &domain::ProfileSnapshot) -> Vec<ChannelGroup> {
|
||||
let company = build_channel_group(snapshot, "COMPANY", |message| message.channel == "company");
|
||||
let department = build_channel_group(snapshot, "DEPARTMENT", |message| {
|
||||
message.channel == "department"
|
||||
});
|
||||
let direct = build_channel_group(snapshot, "DIRECT MESSAGES", |message| {
|
||||
message.channel == "direct"
|
||||
});
|
||||
let task_threads = build_channel_group(snapshot, "TASK DISCUSSIONS", |message| {
|
||||
matches!(message.channel.as_str(), "project" | "incident" | "run")
|
||||
});
|
||||
[company, department, direct, task_threads]
|
||||
.into_iter()
|
||||
.filter(|group| !group.rows.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_channel_group(
|
||||
snapshot: &domain::ProfileSnapshot,
|
||||
label: &'static str,
|
||||
predicate: impl Fn(&domain::AgentMessage) -> bool,
|
||||
) -> ChannelGroup {
|
||||
let mut channels = std::collections::BTreeMap::<String, Vec<&domain::AgentMessage>>::new();
|
||||
for message in snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|message| predicate(message))
|
||||
{
|
||||
channels
|
||||
.entry(message_channel_key(message))
|
||||
.or_default()
|
||||
.push(message);
|
||||
}
|
||||
let rows = channels
|
||||
.into_iter()
|
||||
.map(|(key, messages)| {
|
||||
let label = channel_label(snapshot, messages.as_slice());
|
||||
let count = messages
|
||||
.iter()
|
||||
.filter(|message| message.read_at.is_none())
|
||||
.count();
|
||||
let urgent = messages
|
||||
.iter()
|
||||
.any(|message| message.priority == "urgent" && message.read_at.is_none());
|
||||
let rows = messages
|
||||
.iter()
|
||||
.map(|message| message.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
ChannelRow {
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
urgent,
|
||||
rows,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ChannelGroup { label, rows }
|
||||
}
|
||||
|
||||
fn channel_label(snapshot: &domain::ProfileSnapshot, messages: &[&domain::AgentMessage]) -> String {
|
||||
let Some(first) = messages.first() else {
|
||||
return "#empty".to_string();
|
||||
};
|
||||
match first.channel.as_str() {
|
||||
"company" => "#company-wide".to_string(),
|
||||
"department" => format!(
|
||||
"#{}",
|
||||
first
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "department".to_string())
|
||||
),
|
||||
"direct" => first
|
||||
.to_agent_id
|
||||
.as_ref()
|
||||
.and_then(|agent_id| snapshot.agents.iter().find(|agent| &agent.id == agent_id))
|
||||
.map(|agent| agent.name.clone())
|
||||
.unwrap_or_else(|| "Direct".to_string()),
|
||||
"project" => format!(
|
||||
"#{}",
|
||||
first
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "project".to_string())
|
||||
),
|
||||
"incident" => format!(
|
||||
"#{}",
|
||||
first
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "incident".to_string())
|
||||
),
|
||||
"run" => format!(
|
||||
"Run {}",
|
||||
first
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.get(..8)
|
||||
.unwrap_or("run")
|
||||
),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message_sidebar<'a>(
|
||||
groups: &'a [ChannelGroup],
|
||||
active_channel: Option<&'a str>,
|
||||
selected_message_id: &'a Option<String>,
|
||||
cx: &'a mut Context<AppRoot>,
|
||||
) -> impl IntoElement + 'a {
|
||||
div()
|
||||
.w(px(280.0))
|
||||
.p_3()
|
||||
.gap_3()
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.children(groups.iter().map(|group| {
|
||||
div()
|
||||
.gap_2()
|
||||
.child(message_section_label(group.label))
|
||||
.children(group.rows.iter().map(|channel| {
|
||||
let active = active_channel == Some(channel.key.as_str());
|
||||
let next_message_id = channel
|
||||
.rows
|
||||
.first()
|
||||
.cloned()
|
||||
.or_else(|| selected_message_id.clone())
|
||||
.unwrap_or_default();
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"message-channel-{}",
|
||||
channel.key
|
||||
)))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.rounded_lg()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
if !next_message_id.is_empty() {
|
||||
this.state.selected_message_id = Some(next_message_id.clone());
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.w(px(28.0))
|
||||
.h(px(28.0))
|
||||
.rounded_lg()
|
||||
.bg(theme::panel_muted()),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(if active {
|
||||
theme::sidebar_text_active()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.truncate()
|
||||
.child(channel.label.clone()),
|
||||
),
|
||||
)
|
||||
.when(channel.urgent, |container| {
|
||||
container.child(
|
||||
div()
|
||||
.w(px(8.0))
|
||||
.h(px(8.0))
|
||||
.rounded_full()
|
||||
.bg(theme::danger()),
|
||||
)
|
||||
})
|
||||
.when(channel.count > 0, |container| {
|
||||
container.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_full()
|
||||
.bg(theme::primary_action())
|
||||
.text_xs()
|
||||
.text_color(theme::primary_action_text())
|
||||
.child(channel.count.to_string()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.text_xs()
|
||||
.text_color(theme::text_muted())
|
||||
.child(format!(
|
||||
"{} channels",
|
||||
groups.iter().map(|group| group.rows.len()).sum::<usize>()
|
||||
))
|
||||
.child("message graph"),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message_header(active_channel: Option<&str>, visible_count: usize) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.px_5()
|
||||
.py_4()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_border())
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
div()
|
||||
.text_lg()
|
||||
.text_color(theme::text_primary())
|
||||
.child(active_channel.unwrap_or("Select a channel").to_string()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(4.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(format!("{} visible items", visible_count)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(search_shell("Search messages"))
|
||||
.child(filter_shell()),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message_composer<'a>(
|
||||
root: &'a AppRoot,
|
||||
selected: Option<&'a domain::AgentMessage>,
|
||||
cx: &'a mut Context<AppRoot>,
|
||||
) -> impl IntoElement + 'a {
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.border_t_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(composer_chip(selected_recipient(selected).as_str()))
|
||||
.child(composer_subject(selected)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(12.0))
|
||||
.p_3()
|
||||
.rounded_lg()
|
||||
.bg(theme::panel_inset())
|
||||
.border_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.child(root.message_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(theme::text_muted())
|
||||
.child("Reply, mention, or route an operational update."),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(primary_button("Send update", cx, |this, cx| {
|
||||
this.send_message(cx)
|
||||
}))
|
||||
.child(secondary_button("Mark read", cx, |this, cx| {
|
||||
this.mark_selected_message_read(cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message_inspector(selected: Option<&domain::AgentMessage>) -> impl IntoElement {
|
||||
div()
|
||||
.w(px(320.0))
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.p_5()
|
||||
.child(
|
||||
div()
|
||||
.text_lg()
|
||||
.text_color(theme::text_primary())
|
||||
.child("Thread inspector"),
|
||||
)
|
||||
.child(selected_message_card(selected))
|
||||
.when_some(secondary_message(selected), |container, message| {
|
||||
container.child(compact_message_card(message))
|
||||
})
|
||||
}
|
||||
|
||||
fn message_channel_key(message: &domain::AgentMessage) -> String {
|
||||
match message.channel.as_str() {
|
||||
"company" => "company-all".to_string(),
|
||||
"department" => format!(
|
||||
"department-{}",
|
||||
message
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "general".to_string())
|
||||
),
|
||||
"direct" => {
|
||||
let to = message
|
||||
.to_agent_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "broadcast".to_string());
|
||||
format!("direct-{}-{}", message.from_agent_id, to)
|
||||
}
|
||||
"project" => format!(
|
||||
"project-{}",
|
||||
message
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "general".to_string())
|
||||
),
|
||||
"incident" => format!(
|
||||
"incident-{}",
|
||||
message
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "room".to_string())
|
||||
),
|
||||
"run" => format!(
|
||||
"run-{}",
|
||||
message
|
||||
.channel_target_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "run".to_string())
|
||||
),
|
||||
other => format!("{}-misc", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn messages_for_channel<'a>(
|
||||
snapshot: &'a domain::ProfileSnapshot,
|
||||
channel_key: &str,
|
||||
) -> Vec<&'a domain::AgentMessage> {
|
||||
snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|message| message_channel_key(message) == channel_key)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
@@ -183,7 +183,7 @@
|
||||
],
|
||||
"priority": 13,
|
||||
"passes": false,
|
||||
"notes": "In progress: native Document Center now restores a two-section grammar with documents prose plus work products (sourced from persisted artifacts), instead of the earlier flat list plus quick draft shell. Remaining gap is finer editor/message parity and final source-to-native audit."
|
||||
"notes": "In progress: native Document Center now separates documents prose and work products using persisted artifacts. Messages now restores a more source-like communication shell with grouped channels, unread badges, a channel header, a message stream, a composer area, and a side inspector. Remaining work is the final source-to-native parity audit and any last micro-gaps it surfaces."
|
||||
},
|
||||
{
|
||||
"id": "US-014",
|
||||
@@ -197,7 +197,7 @@
|
||||
],
|
||||
"priority": 14,
|
||||
"passes": false,
|
||||
"notes": "Created after post-completion audit found significant remaining UI grammar gaps."
|
||||
"notes": "Audit not yet finalized. The post-completion remediation cycle has now rebuilt onboarding, dashboard, inbox, approvals, runs, documents, and messages toward source grammar. Next step is a final explicit audit pass to decide which remaining deviations are still material."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,4 +14,4 @@ Started: 2026-04-19
|
||||
- Added runtime_contract_snapshot_e2e.rs to verify runtime event and artifact persistence across reopen.
|
||||
- Runtime contract integration advanced: RuntimeEvent ingestion now persists message/tool/artifact/run-finished events into storage and exposes them in the runs inspector.
|
||||
- Wired a live native runtime session into the GPUI Runs surface. Selected runs now launch Pi Mono through env-configured stdio sidecar settings, runtime tool calls create approval rows, approval decisions send ToolApproval back into the live session, runtime messages project into agent_messages, and run lifecycle still persists into run_events/artifacts/runs.\n- Closed the final native verification matrix. The workspace now has explicit storage-backed e2e coverage for onboarding bootstrap, approvals/documents/runs visibility, settings/workspace mutations, live runtime contract flows, and inbox attention derivation.\n- Reopened Ralph parity work after a direct source audit. The current native app has real runtime/data flows, but several surfaces still diverge from Electron page grammar: onboarding is too collapsed, dashboard misses banner/attention/chart structure, inbox filters are not real, run detail is dominated by native control slabs not present in source, approvals flatten board/user/manager semantics, and documents still lack work-product/editor hierarchy. Added US-010 through US-014 to close these parity gaps honestly.
|
||||
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n- Restored more source grammar across inbox, approvals, and runs. Inbox now has real interactive filters driving a unified attention stream; approvals again distinguish board/user/manager review groupings; and run detail no longer lets live runtime controls dominate the main header region.\n- Restored document-center grammar toward the source surface: the native page now separates documents prose from work products and uses persisted artifacts as the work-product list instead of hiding them behind the runs inspector only.\n
|
||||
- Rebuilt native onboarding away from the old static split shell. It now has a real multi-stage company/mission/launch flow, screenshot-aligned narrative/summary composition, real connector readiness derived from snapshot data, and a storage bootstrap transaction that creates company, workspace, goal, CEO, kickoff project, kickoff task, and queued run. Remaining US-010 work is the finer original wizard-tab parity.\n- Rebuilt the native dashboard away from the placeholder metrics/activity shell. It now has real recovery and budget banners, attention rows, a performance section, active work, and recent activity, all driven by persisted local data. Remaining US-011 work is the inbox-side parity and finer dashboard detail matching.\n- Restored more source grammar across inbox, approvals, and runs. Inbox now has real interactive filters driving a unified attention stream; approvals again distinguish board/user/manager review groupings; and run detail no longer lets live runtime controls dominate the main header region.\n- Restored document-center grammar toward the source surface: the native page now separates documents prose from work products and uses persisted artifacts as the work-product list instead of hiding them behind the runs inspector only.\n- Restored the communication shell toward the source product grammar: grouped channels, unread badges, channel header, message stream, composer, and side inspector. Combined with the earlier document-center rewrite, the remaining work is now the final parity audit rather than major page-grammar gaps.\n
|
||||
Reference in new issue
Block a user