refactor(collections): split prototype modules

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-08-27 17:54:54 -04:00
parent 569b4f001d
commit 972d73dd9f
5 changed files with 1546 additions and 1534 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,848 @@
use std::{collections::HashSet, path::Path, process::Stdio, time::Duration};
use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio::process::Command;
const CALENDAR_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(20);
const CALENDAR_TOOL_TIMEOUT_SECS: &str = "15";
const DRIVE_PROCESS_TIMEOUT: Duration = Duration::from_secs(12);
const DRIVE_TOOL_TIMEOUT_SECS: &str = "10";
const CALENDAR_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(45);
const MAX_ACTIVITY_DOCUMENTS: usize = 16;
const MAX_CALENDAR_STDOUT_BYTES: usize = 1024 * 1024;
const MAX_CALENDAR_STDERR_BYTES: usize = 64 * 1024;
const CALENDAR_UNAVAILABLE: &str = "Optional Google Calendar discovery is unavailable";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CollectionDiscoveredLink {
pub url: String,
pub label: String,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CollectionCalendarActivity {
pub action_type: String,
pub timestamp: String,
pub actor_display_name: Option<String>,
pub actor_email: Option<String>,
pub document_title: String,
pub document_url: String,
pub document_file_id: String,
pub source_calendar_url: String,
pub source_attachment_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CollectionActivitySourceError {
pub source_url: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CollectionCalendarActivityResult {
pub activities: Vec<CollectionCalendarActivity>,
pub errors: Vec<CollectionActivitySourceError>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DriveDocumentMetadata {
file_id: String,
title: String,
url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedDriveActivity {
action_type: String,
timestamp: String,
actor_display_name: Option<String>,
actor_email: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CalendarToolResponse {
#[serde(default)]
result: Option<CalendarEventResult>,
#[serde(default)]
attachments: Vec<CalendarAttachment>,
}
#[derive(Debug, Deserialize)]
struct CalendarEventResult {
#[serde(default)]
attachments: Vec<CalendarAttachment>,
}
#[derive(Debug, Deserialize)]
struct CalendarAttachment {
#[serde(rename = "fileUrl")]
file_url: String,
#[serde(default)]
title: String,
}
/// Discover current document attachments for a linked Google Calendar event.
///
/// Collections keep the calendar event as the explicit membership edge. This
/// optional local resolver reads the user's already-connected Calendar account
/// and returns ephemeral children; it never writes those children to SQLite.
#[tauri::command]
pub async fn discover_collection_calendar_links(
url: String,
) -> Result<Vec<CollectionDiscoveredLink>, String> {
validate_google_calendar_event_url(&url)?;
let sq_path = crate::managed_agents::resolve_command("sq")
.ok_or_else(|| format!("{CALENDAR_UNAVAILABLE}: `sq` was not found on this device"))?;
fetch_calendar_links(&sq_path, &url).await
}
/// Discover ephemeral edit and comment activity for Google Drive documents
/// attached to an explicit Google Calendar event.
#[tauri::command]
pub async fn discover_collection_calendar_activity(
calendar_url: String,
start_time: String,
end_time: Option<String>,
) -> Result<CollectionCalendarActivityResult, String> {
validate_google_calendar_event_url(&calendar_url)?;
validate_activity_window(&start_time, end_time.as_deref())?;
let sq_path = crate::managed_agents::resolve_command("sq").ok_or_else(|| {
format!(
"Optional Google Calendar activity discovery is unavailable for {calendar_url}: `sq` was not found on this device"
)
})?;
tokio::time::timeout(
CALENDAR_ACTIVITY_TIMEOUT,
discover_calendar_activity(&sq_path, &calendar_url, &start_time, end_time.as_deref()),
)
.await
.map_err(|_| {
format!(
"Optional Google Calendar activity discovery is unavailable for {calendar_url}: lookup timed out"
)
})?
}
async fn fetch_calendar_links(
sq_path: &Path,
url: &str,
) -> Result<Vec<CollectionDiscoveredLink>, String> {
let args = [
"agent-tools",
"google-calendar",
"get-calendar-event",
"--event-id",
url,
"--calendar-id",
"primary",
"--output",
"json",
"--timeout",
CALENDAR_TOOL_TIMEOUT_SECS,
];
let stdout = run_sq_command(
sq_path,
&args,
CALENDAR_DISCOVERY_TIMEOUT,
CALENDAR_UNAVAILABLE,
)
.await?;
parse_calendar_links(&stdout).map_err(|error| format!("{CALENDAR_UNAVAILABLE}: {error}"))
}
async fn discover_calendar_activity(
sq_path: &Path,
calendar_url: &str,
start_time: &str,
end_time: Option<&str>,
) -> Result<CollectionCalendarActivityResult, String> {
let links = fetch_calendar_links(sq_path, calendar_url)
.await
.map_err(|error| {
format!(
"Optional Google Calendar activity discovery is unavailable for {calendar_url}: {error}"
)
})?;
let mut documents = Vec::new();
let mut seen_file_ids = HashSet::new();
for link in links {
let Some(file_id) = google_drive_file_id(&link.url) else {
continue;
};
if seen_file_ids.insert(file_id.clone()) {
documents.push((file_id, link.url, link.label));
}
}
let mut result = CollectionCalendarActivityResult {
activities: Vec::new(),
errors: Vec::new(),
};
if documents.len() > MAX_ACTIVITY_DOCUMENTS {
result.errors.push(CollectionActivitySourceError {
source_url: calendar_url.to_string(),
message: format!(
"Only the first {MAX_ACTIVITY_DOCUMENTS} attached Drive documents were checked"
),
});
documents.truncate(MAX_ACTIVITY_DOCUMENTS);
}
for (file_id, attachment_url, attachment_label) in documents {
let metadata = match fetch_drive_metadata(sq_path, &file_id).await {
Ok(metadata) => metadata,
Err(message) => {
result.errors.push(CollectionActivitySourceError {
source_url: attachment_url,
message: format!("{attachment_label} ({file_id}): {message}"),
});
continue;
}
};
let activities = match fetch_drive_activity(sq_path, &file_id, start_time, end_time).await {
Ok(activities) => activities,
Err(message) => {
result.errors.push(CollectionActivitySourceError {
source_url: attachment_url,
message: format!("{} ({file_id}): {message}", metadata.title),
});
continue;
}
};
result
.activities
.extend(
activities
.into_iter()
.map(|activity| CollectionCalendarActivity {
action_type: activity.action_type,
timestamp: activity.timestamp,
actor_display_name: activity.actor_display_name,
actor_email: activity.actor_email,
document_title: metadata.title.clone(),
document_url: metadata.url.clone(),
document_file_id: metadata.file_id.clone(),
source_calendar_url: calendar_url.to_string(),
source_attachment_url: attachment_url.clone(),
}),
);
}
result.activities.sort_by(|left, right| {
right
.timestamp
.cmp(&left.timestamp)
.then_with(|| left.document_file_id.cmp(&right.document_file_id))
.then_with(|| left.action_type.cmp(&right.action_type))
});
result.activities.dedup_by(|left, right| {
left.document_file_id == right.document_file_id
&& left.action_type == right.action_type
&& left.timestamp == right.timestamp
&& left.actor_email == right.actor_email
&& left.actor_display_name == right.actor_display_name
});
Ok(result)
}
async fn fetch_drive_metadata(
sq_path: &Path,
file_id: &str,
) -> Result<DriveDocumentMetadata, String> {
let args = [
"agent-tools",
"google-drive",
"get-file-metadata",
"--file-id-or-url",
file_id,
"--raw",
"--timeout",
DRIVE_TOOL_TIMEOUT_SECS,
];
let unavailable = "Optional Google Drive metadata is unavailable";
let output = run_sq_command(sq_path, &args, DRIVE_PROCESS_TIMEOUT, unavailable).await?;
parse_drive_metadata(&output, file_id).map_err(|error| format!("{unavailable}: {error}"))
}
async fn fetch_drive_activity(
sq_path: &Path,
file_id: &str,
start_time: &str,
end_time: Option<&str>,
) -> Result<Vec<ParsedDriveActivity>, String> {
let mut args = vec![
"agent-tools",
"google-drive",
"activity",
"--start-time",
start_time,
];
if let Some(end_time) = end_time {
args.extend(["--end-time", end_time]);
}
args.extend([
"--item-id",
file_id,
"--activity-type",
"edit",
"comment",
"--lookup-people",
"--raw",
"--timeout",
DRIVE_TOOL_TIMEOUT_SECS,
]);
let unavailable = "Optional Google Drive activity is unavailable";
let output = run_sq_command(sq_path, &args, DRIVE_PROCESS_TIMEOUT, unavailable).await?;
parse_drive_activity(&output, file_id).map_err(|error| format!("{unavailable}: {error}"))
}
async fn run_sq_command(
sq_path: &Path,
args: &[&str],
process_timeout: Duration,
unavailable: &str,
) -> Result<Vec<u8>, String> {
let mut command = Command::new(sq_path);
command
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut child = command
.spawn()
.map_err(|error| format!("{unavailable}: failed to start `sq`: {error}"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| format!("{unavailable}: failed to capture `sq` output"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| format!("{unavailable}: failed to capture `sq` errors"))?;
let (status, stdout, stderr) = tokio::time::timeout(process_timeout, async move {
let (stdout, stderr, status) = tokio::join!(
read_bounded(stdout, MAX_CALENDAR_STDOUT_BYTES),
read_bounded(stderr, MAX_CALENDAR_STDERR_BYTES),
child.wait(),
);
Ok::<_, std::io::Error>((status?, stdout?, stderr?))
})
.await
.map_err(|_| format!("{unavailable}: lookup timed out"))?
.map_err(|error| format!("{unavailable}: failed while running `sq`: {error}"))?;
if !status.success() {
return Err(format!(
"{unavailable}: {}",
first_safe_line(&stderr).unwrap_or("tool lookup failed")
));
}
Ok(stdout)
}
async fn read_bounded(
reader: impl AsyncRead + Unpin,
max_bytes: usize,
) -> Result<Vec<u8>, std::io::Error> {
let mut bytes = Vec::new();
reader
.take((max_bytes + 1) as u64)
.read_to_end(&mut bytes)
.await?;
if bytes.len() > max_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"tool response exceeded the size limit",
));
}
Ok(bytes)
}
fn first_safe_line(stderr: &[u8]) -> Option<&str> {
std::str::from_utf8(stderr)
.ok()?
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.chars().any(char::is_control))
}
fn validate_google_calendar_event_url(raw: &str) -> Result<(), String> {
if raw != raw.trim() || raw.len() > 8 * 1024 || raw.chars().any(char::is_control) {
return Err("invalid Google Calendar event URL".to_string());
}
let url = url::Url::parse(raw).map_err(|_| "invalid Google Calendar event URL".to_string())?;
let is_google_host = matches!(
url.host_str(),
Some("www.google.com" | "calendar.google.com")
);
let event_ids = url
.query_pairs()
.filter(|(key, _)| key == "eid")
.collect::<Vec<_>>();
if url.scheme() != "https"
|| !is_google_host
|| !url.username().is_empty()
|| url.password().is_some()
|| url.port().is_some()
|| url.path() != "/calendar/event"
|| url.fragment().is_some()
|| event_ids.len() != 1
|| event_ids[0].1.is_empty()
|| event_ids[0].1.chars().any(char::is_control)
{
return Err("URL is not a supported Google Calendar event".to_string());
}
Ok(())
}
fn validate_activity_window(start_time: &str, end_time: Option<&str>) -> Result<(), String> {
let start = DateTime::parse_from_rfc3339(start_time)
.map_err(|_| "invalid Drive activity start time; expected RFC 3339".to_string())?;
if let Some(end_time) = end_time {
let end = DateTime::parse_from_rfc3339(end_time)
.map_err(|_| "invalid Drive activity end time; expected RFC 3339".to_string())?;
if end <= start {
return Err("Drive activity end time must be after start time".to_string());
}
}
Ok(())
}
fn google_drive_file_id(raw: &str) -> Option<String> {
if raw != raw.trim() || raw.len() > 8 * 1024 || raw.chars().any(char::is_control) {
return None;
}
let url = url::Url::parse(raw).ok()?;
if url.scheme() != "https"
|| !url.username().is_empty()
|| url.password().is_some()
|| url.port().is_some()
|| url.fragment().is_some()
{
return None;
}
let host = url.host_str()?;
let segments = url.path_segments()?.collect::<Vec<_>>();
let candidate = match host {
"docs.google.com"
if segments.first() == Some(&"document") && segments.get(1) == Some(&"d") =>
{
segments.get(2).copied()
}
"drive.google.com"
if segments.first() == Some(&"file") && segments.get(1) == Some(&"d") =>
{
segments.get(2).copied()
}
"drive.google.com" if url.path() == "/open" => {
let ids = url
.query_pairs()
.filter(|(key, _)| key == "id")
.map(|(_, value)| value.into_owned())
.collect::<Vec<_>>();
if ids.len() == 1 {
return valid_drive_file_id(&ids[0]).then(|| ids[0].clone());
}
None
}
_ => None,
}?;
valid_drive_file_id(candidate).then(|| candidate.to_string())
}
fn valid_drive_file_id(candidate: &str) -> bool {
(10..=128).contains(&candidate.len())
&& candidate
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
}
fn parse_drive_metadata(
output: &[u8],
expected_file_id: &str,
) -> Result<DriveDocumentMetadata, String> {
let value: Value = serde_json::from_slice(output)
.map_err(|error| format!("invalid Drive metadata response: {error}"))?;
if let Some(metadata) = find_drive_metadata(&value, expected_file_id) {
return Ok(metadata);
}
let mut texts = Vec::new();
collect_content_text(&value, &mut texts);
texts
.iter()
.find_map(|text| parse_drive_metadata_text(text, expected_file_id))
.ok_or_else(|| "metadata did not identify the attached Google document".to_string())
}
fn find_drive_metadata(value: &Value, expected_file_id: &str) -> Option<DriveDocumentMetadata> {
match value {
Value::Object(object) => {
let file_id = ["id", "fileId", "file_id"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str));
let mime_type = ["mimeType", "mime_type"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str));
let title = ["name", "title"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str));
let document_url = ["webViewLink", "web_view_link", "url"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_str));
if file_id == Some(expected_file_id)
&& mime_type == Some("application/vnd.google-apps.document")
{
let title = title?.trim();
let document_url = document_url?.trim();
if !title.is_empty()
&& !title.chars().any(char::is_control)
&& google_drive_file_id(document_url).as_deref() == Some(expected_file_id)
{
return Some(DriveDocumentMetadata {
file_id: expected_file_id.to_string(),
title: title.to_string(),
url: document_url.to_string(),
});
}
}
for child in object.values() {
if let Some(metadata) = find_drive_metadata(child, expected_file_id) {
return Some(metadata);
}
}
None
}
Value::Array(values) => values
.iter()
.find_map(|child| find_drive_metadata(child, expected_file_id)),
Value::String(text) => parse_embedded_json(text)
.as_ref()
.and_then(|embedded| find_drive_metadata(embedded, expected_file_id)),
_ => None,
}
}
fn parse_embedded_json(text: &str) -> Option<Value> {
let trimmed = text.trim();
let trimmed = trimmed
.strip_prefix("```json")
.or_else(|| trimmed.strip_prefix("```"))
.unwrap_or(trimmed)
.strip_suffix("```")
.unwrap_or(trimmed)
.trim();
serde_json::from_str(trimmed).ok()
}
fn parse_drive_metadata_text(text: &str, expected_file_id: &str) -> Option<DriveDocumentMetadata> {
let mut file_id = None;
let mut title = None;
let mut mime_type = None;
let mut document_url = None;
for line in text.lines() {
let line = line.trim().trim_start_matches(['-', '*', '#']).trim();
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key
.trim_matches(|character: char| {
matches!(character, '*' | '`') || character.is_whitespace()
})
.to_ascii_lowercase();
let value = value.trim_matches(|character: char| {
matches!(character, '*' | '`') || character.is_whitespace()
});
match key.as_str() {
"id" | "file id" => file_id = Some(value),
"name" | "title" => title = Some(value),
"mime type" | "mimetype" => mime_type = Some(value),
"web view link" | "webviewlink" | "url" => document_url = Some(value),
_ => {}
}
}
if file_id != Some(expected_file_id)
|| mime_type != Some("application/vnd.google-apps.document")
{
return None;
}
let title = title?;
let document_url = document_url?;
if title.is_empty()
|| title.chars().any(char::is_control)
|| google_drive_file_id(document_url).as_deref() != Some(expected_file_id)
{
return None;
}
Some(DriveDocumentMetadata {
file_id: expected_file_id.to_string(),
title: title.to_string(),
url: document_url.to_string(),
})
}
fn parse_drive_activity(
output: &[u8],
expected_file_id: &str,
) -> Result<Vec<ParsedDriveActivity>, String> {
let Some(text) = drive_activity_text(output)? else {
return Ok(Vec::new());
};
let mut activities = text
.lines()
.filter_map(|line| parse_drive_activity_line(line, expected_file_id))
.collect::<Vec<_>>();
activities.sort_by(|left, right| right.timestamp.cmp(&left.timestamp));
activities.dedup();
if activities.is_empty() && !is_empty_activity_text(&text) {
return Err(
"Drive activity response contained no recognized edit or comment events".into(),
);
}
Ok(activities)
}
fn is_empty_activity_text(text: &str) -> bool {
let text = text.trim();
if text.is_empty() {
return true;
}
let lowercase = text.to_ascii_lowercase();
if lowercase.contains("no activity")
|| lowercase.contains("no recent activity")
|| lowercase.contains("no drive activity")
|| lowercase.contains("no matching activity")
|| lowercase.contains("no events")
{
return true;
}
parse_embedded_json(text)
.as_ref()
.is_some_and(is_explicit_empty_activity_value)
}
fn drive_activity_text(output: &[u8]) -> Result<Option<String>, String> {
let raw = std::str::from_utf8(output)
.map_err(|_| "Drive activity response was not UTF-8".to_string())?;
if raw.trim().is_empty() {
return Ok(None);
}
let Ok(value) = serde_json::from_str::<Value>(raw) else {
return Ok(Some(raw.to_string()));
};
let mut texts = Vec::new();
collect_content_text(&value, &mut texts);
texts.retain(|text| !text.trim().is_empty());
if texts.is_empty() && is_explicit_empty_activity_response(&value) {
return Ok(None);
}
if texts.is_empty() {
return Err("Drive activity response did not contain text content".into());
}
Ok(Some(texts.join("\n")))
}
fn is_explicit_empty_activity_response(value: &Value) -> bool {
match value {
Value::Object(object) => {
if object.get("is_error").and_then(Value::as_bool) == Some(true)
|| object.get("isError").and_then(Value::as_bool) == Some(true)
{
return false;
}
[
"content",
"activities",
"activity",
"events",
"items",
"result",
]
.iter()
.any(|key| {
object
.get(*key)
.is_some_and(is_explicit_empty_activity_value)
})
}
_ => false,
}
}
fn is_explicit_empty_activity_value(value: &Value) -> bool {
match value {
Value::Null => true,
Value::Array(values) => {
values.is_empty() || values.iter().all(is_explicit_empty_activity_value)
}
Value::String(text) => text.trim().is_empty(),
Value::Object(object) => {
if let Some(text) = object.get("text") {
return is_explicit_empty_activity_value(text);
}
["activities", "activity", "events", "items", "result"]
.iter()
.any(|key| {
object
.get(*key)
.is_some_and(is_explicit_empty_activity_value)
})
}
_ => false,
}
}
fn collect_content_text(value: &Value, texts: &mut Vec<String>) {
match value {
Value::Object(object) => {
if let Some(text) = object.get("text").and_then(Value::as_str) {
texts.push(text.to_string());
return;
}
for child in object.values() {
collect_content_text(child, texts);
}
}
Value::Array(values) => {
for child in values {
collect_content_text(child, texts);
}
}
_ => {}
}
}
fn parse_drive_activity_line(line: &str, expected_file_id: &str) -> Option<ParsedDriveActivity> {
let line = line.trim().strip_prefix("- ").unwrap_or(line.trim());
let after_open = line.strip_prefix('[')?;
let (timestamp, rest) = after_open.split_once("] ")?;
let timestamp = parse_drive_activity_timestamp(timestamp)?;
let (action, rest) = rest.split_once(" by ")?;
let action = action.to_ascii_lowercase();
let action_type = if action.contains("comment") {
"comment"
} else if action.contains("edit") {
"edit"
} else {
return None;
};
let (actor, target) = rest.split_once(" on \"")?;
let (_, detail) = target.rsplit_once("\" (")?;
let reported_file_id = detail
.split("id: ")
.nth(1)?
.split([',', ')'])
.next()?
.trim();
let file_id = reported_file_id
.strip_prefix("items/")
.unwrap_or(reported_file_id);
if file_id != expected_file_id || !valid_drive_file_id(file_id) {
return None;
}
let (actor_display_name, actor_email) = parse_drive_actor(actor.trim());
Some(ParsedDriveActivity {
action_type: action_type.to_string(),
timestamp,
actor_display_name,
actor_email,
})
}
fn parse_drive_activity_timestamp(raw: &str) -> Option<String> {
let timestamp = DateTime::parse_from_rfc3339(raw)
.map(|timestamp| timestamp.with_timezone(&Utc))
.or_else(|_| {
NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f")
.map(|timestamp| timestamp.and_utc())
})
.ok()?;
Some(timestamp.to_rfc3339_opts(SecondsFormat::Millis, true))
}
fn parse_drive_actor(actor: &str) -> (Option<String>, Option<String>) {
let actor = actor.trim();
if let Some((name, email)) = actor
.strip_suffix(')')
.and_then(|value| value.rsplit_once(" ("))
{
if valid_actor_email(email) {
return (nonempty_actor_name(name), Some(email.to_string()));
}
}
if let Some((name, email)) = actor
.strip_suffix('>')
.and_then(|value| value.rsplit_once(" <"))
{
if valid_actor_email(email) {
return (nonempty_actor_name(name), Some(email.to_string()));
}
}
if valid_actor_email(actor) {
return (None, Some(actor.to_string()));
}
(nonempty_actor_name(actor), None)
}
fn nonempty_actor_name(name: &str) -> Option<String> {
let name = name.trim();
(!name.is_empty() && name.len() <= 256 && !name.chars().any(char::is_control))
.then(|| name.to_string())
}
fn valid_actor_email(email: &str) -> bool {
email.len() <= 254
&& email.contains('@')
&& !email
.chars()
.any(|character| character.is_whitespace() || character.is_control())
}
fn parse_calendar_links(output: &[u8]) -> Result<Vec<CollectionDiscoveredLink>, String> {
let response: CalendarToolResponse = serde_json::from_slice(output)
.map_err(|error| format!("invalid Google Calendar response: {error}"))?;
let attachments = response
.result
.map_or(response.attachments, |result| result.attachments);
let mut links = Vec::new();
for attachment in attachments {
if attachment.file_url != attachment.file_url.trim()
|| attachment.file_url.chars().any(char::is_control)
{
continue;
}
let Ok(url) = url::Url::parse(&attachment.file_url) else {
continue;
};
if !matches!(url.scheme(), "http" | "https")
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| google_drive_file_id(url.as_str()).is_none()
{
continue;
}
links.push(CollectionDiscoveredLink {
url: url.to_string(),
label: if attachment.title.trim().is_empty() {
url.host_str().unwrap_or_default().to_string()
} else {
attachment.title.trim().to_string()
},
kind: "document".to_string(),
});
}
links.sort_by(|left, right| left.url.cmp(&right.url));
links.dedup_by(|left, right| left.url == right.url);
Ok(links)
}
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,361 @@
use super::super::{
parse_uuid, AddCollectionMemberInput, CreateCollectionInput, SetCollectionIconInput,
SetCollectionNameInput,
};
use super::*;
use buzz_collections_pkg::CollectionReference;
#[test]
fn command_ids_are_validated_before_store_access() {
assert!(parse_uuid("not-a-uuid", "collection").is_err());
assert!(parse_uuid("9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", "collection").is_ok());
}
#[test]
fn add_member_input_accepts_desktop_thread_payload() {
let input: AddCollectionMemberInput = serde_json::from_value(serde_json::json!({
"relay_url": "wss://buzz.block.builderlab.xyz",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"collection_id": "593a7412-49ee-4709-ac66-17f6e6288279",
"reference": {
"type": "thread",
"channel_id": "7f4a6441-4028-4cf5-8872-de456856d68e",
"root_event_id": "b016509b88c9c1cdc615f7101b779104a3101758f54e6a48a10a2e51843ff358"
},
"label": "Thread: production payload"
}))
.expect("desktop thread payload should deserialize");
assert!(matches!(
input.reference,
CollectionReference::Thread { .. }
));
}
#[test]
fn icon_inputs_accept_create_set_and_clear_payloads() {
let create: CreateCollectionInput = serde_json::from_value(serde_json::json!({
"relay_url": "wss://buzz.block.builderlab.xyz",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"name": "Bird Voice",
"description": null,
"icon": "🐦"
}))
.expect("create icon payload");
assert_eq!(create.icon.as_deref(), Some("🐦"));
let set: SetCollectionIconInput = serde_json::from_value(serde_json::json!({
"relay_url": "wss://buzz.block.builderlab.xyz",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"collection_id": "593a7412-49ee-4709-ac66-17f6e6288279",
"icon": "🎧"
}))
.expect("set icon payload");
assert_eq!(set.icon.as_deref(), Some("🎧"));
let clear: SetCollectionIconInput = serde_json::from_value(serde_json::json!({
"relay_url": "wss://buzz.block.builderlab.xyz",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"collection_id": "593a7412-49ee-4709-ac66-17f6e6288279",
"icon": null
}))
.expect("clear icon payload");
assert_eq!(clear.icon, None);
}
#[test]
fn rename_input_accepts_desktop_payload() {
let input: SetCollectionNameInput = serde_json::from_value(serde_json::json!({
"relay_url": "wss://buzz.block.builderlab.xyz",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"collection_id": "593a7412-49ee-4709-ac66-17f6e6288279",
"name": "Berd Voice"
}))
.expect("rename payload");
assert_eq!(input.name, "Berd Voice");
}
#[test]
fn calendar_discovery_only_accepts_google_event_links() {
assert!(validate_google_calendar_event_url(
"https://www.google.com/calendar/event?eid=abc123&ctz=UTC"
)
.is_ok());
assert!(
validate_google_calendar_event_url("https://example.com/calendar/event?eid=abc").is_err()
);
assert!(validate_google_calendar_event_url("https://www.google.com/calendar/event").is_err());
assert!(validate_google_calendar_event_url(
"https://attacker@example.com/calendar/event?eid=abc"
)
.is_err());
assert!(validate_google_calendar_event_url(
"https://www.google.com:444/calendar/event?eid=abc"
)
.is_err());
assert!(validate_google_calendar_event_url(
"https://www.google.com/calendar/event?eid=abc#fragment"
)
.is_err());
assert!(validate_google_calendar_event_url(
"https://www.google.com/calendar/event?eid=abc&eid=second"
)
.is_err());
}
#[test]
fn calendar_discovery_returns_document_links_without_persisting_them() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let output = serde_json::json!({
"result": {
"attachments": [
{"fileUrl": format!("https://docs.google.com/document/d/{file_id}/edit"), "title": "Notes by Gemini"},
{"fileUrl": "file:///tmp/private", "title": "Local file"},
{"fileUrl": "https://user:secret@example.com/private", "title": "Credentials"},
{"fileUrl": "https://example.com/meeting-notes.pdf", "title": "Arbitrary PDF"},
{"fileUrl": "https://docs.google.com/document/d/too-short/edit", "title": "Malformed Drive ID"},
{"fileUrl": format!("https://docs.google.com/document/d/{file_id}/edit"), "title": "Duplicate"}
]
}
});
let links = parse_calendar_links(output.to_string().as_bytes()).expect("calendar links");
assert_eq!(
links,
vec![CollectionDiscoveredLink {
url: format!("https://docs.google.com/document/d/{file_id}/edit"),
label: "Notes by Gemini".to_string(),
kind: "document".to_string(),
}]
);
}
#[test]
fn calendar_discovery_accepts_direct_event_json() {
let drive_file_id = "1DriveFileAttachmentId_123456789";
let docs_file_id = "1DocsFileAttachmentId_1234567890";
let output = serde_json::json!({
"attachments": [
{"fileUrl": format!("https://drive.google.com/file/d/{drive_file_id}/view"), "title": " Drive file "},
{"fileUrl": "javascript:alert(1)", "title": "Unsafe"},
{"fileUrl": format!("https://docs.google.com/document/d/{docs_file_id}/edit"), "title": ""},
{"fileUrl": "https://example.com/no-title", "title": "Website"}
]
});
let links = parse_calendar_links(output.to_string().as_bytes()).expect("calendar links");
assert_eq!(links.len(), 2);
assert_eq!(links[0].label, "docs.google.com");
assert_eq!(links[1].label, "Drive file");
}
#[test]
fn drive_file_ids_only_come_from_strict_attachment_urls() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
assert_eq!(
google_drive_file_id(&format!(
"https://docs.google.com/document/d/{file_id}/edit"
)),
Some(file_id.to_string())
);
assert_eq!(
google_drive_file_id(&format!("https://drive.google.com/file/d/{file_id}/view")),
Some(file_id.to_string())
);
assert_eq!(
google_drive_file_id(&format!("https://drive.google.com/open?id={file_id}")),
Some(file_id.to_string())
);
assert!(google_drive_file_id(&format!(
"https://docs.google.com.evil.example/document/d/{file_id}/edit"
))
.is_none());
assert!(google_drive_file_id(&format!(
"https://user@docs.google.com/document/d/{file_id}/edit"
))
.is_none());
assert!(google_drive_file_id(&format!(
"https://docs.google.com/spreadsheets/d/{file_id}/edit"
))
.is_none());
assert!(google_drive_file_id("https://drive.google.com/file/d/too-short/view").is_none());
}
#[test]
fn drive_metadata_must_match_the_attached_google_doc() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let output = serde_json::json!({
"content": [{"type": "text", "text": "metadata loaded"}],
"structuredContent": {
"id": file_id,
"name": "Launch notes",
"mimeType": "application/vnd.google-apps.document",
"webViewLink": format!("https://docs.google.com/document/d/{file_id}/edit")
}
});
let metadata = parse_drive_metadata(output.to_string().as_bytes(), file_id).expect("metadata");
assert_eq!(metadata.title, "Launch notes");
assert_eq!(metadata.file_id, file_id);
assert!(parse_drive_metadata(output.to_string().as_bytes(), "differentFileId12345").is_err());
}
#[test]
fn drive_metadata_accepts_formatted_raw_content() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let output = serde_json::json!({
"content": [{
"type": "text",
"text": format!(
"## File metadata\n- **ID:** `{file_id}`\n- **Name:** Launch notes\n- **MIME type:** application/vnd.google-apps.document\n- **Web view link:** https://docs.google.com/document/d/{file_id}/edit"
)
}]
});
let metadata = parse_drive_metadata(output.to_string().as_bytes(), file_id).expect("metadata");
assert_eq!(metadata.title, "Launch notes");
}
#[test]
fn drive_activity_parses_realistic_edit_and_comment_output() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let raw = serde_json::json!({
"content": [{
"text": {
"text": format!(
"- [2026-08-27T13:42:01Z] EDIT by Ada Lovelace (ada@example.com) on \"Launch notes\" (type: drive#file, id: {file_id})\n- [2026-08-27T14:05:03-04:00] COMMENT by Grace Hopper <grace@example.com> on \"Launch notes\" (type: drive#file, id: {file_id})\n- [2026-08-27T14:06:00Z] RENAME by Someone on \"Launch notes\" (type: drive#file, id: {file_id})\n- [2026-08-27T14:07:00Z] EDIT by Attacker on \"Other\" (type: drive#file, id: 1OtherValidatedFileId987654321)"
)
}
}]
});
let activities = parse_drive_activity(raw.to_string().as_bytes(), file_id).expect("activity");
assert_eq!(activities.len(), 2);
assert_eq!(activities[0].action_type, "comment");
assert_eq!(activities[0].timestamp, "2026-08-27T18:05:03.000Z");
assert_eq!(
activities[0].actor_display_name.as_deref(),
Some("Grace Hopper")
);
assert_eq!(
activities[0].actor_email.as_deref(),
Some("grace@example.com")
);
assert_eq!(activities[1].action_type, "edit");
assert_eq!(
activities[1].actor_display_name.as_deref(),
Some("Ada Lovelace")
);
assert_eq!(
activities[1].actor_email.as_deref(),
Some("ada@example.com")
);
}
#[test]
fn drive_activity_parses_live_naive_utc_timestamp_and_item_id() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let raw = serde_json::json!({
"content": [{
"text": {
"text": format!(
"- [2026-08-27 18:09:30] EDIT by Ada Lovelace (ada@example.com) on \"Launch notes\" (type: drive#file, id: items/{file_id})\n- [2026-08-27 18:10:31] COMMENT by Grace Hopper on \"Launch notes\" (type: drive#file, id: items/{file_id})\n- [2026-08-27 18:11:32] EDIT by Wrong File on \"Other\" (type: drive#file, id: items/1OtherValidatedFileId987654321)"
)
}
}],
"is_error": false
});
let activities =
parse_drive_activity(raw.to_string().as_bytes(), file_id).expect("live activity");
assert_eq!(activities.len(), 2);
assert_eq!(activities[0].action_type, "comment");
assert_eq!(activities[0].timestamp, "2026-08-27T18:10:31.000Z");
assert_eq!(activities[1].action_type, "edit");
assert_eq!(activities[1].timestamp, "2026-08-27T18:09:30.000Z");
}
#[test]
fn drive_activity_accepts_explicit_empty_structured_responses() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
for output in [
serde_json::json!({"content": [], "is_error": false}),
serde_json::json!({
"content": [{"text": {"text": ""}}],
"is_error": false
}),
serde_json::json!({
"content": [],
"structuredContent": {"activities": []},
"is_error": false
}),
serde_json::json!({
"content": [{"text": {"text": "No Drive activity found."}}],
"is_error": false
}),
serde_json::json!({
"content": [
{"text": {"text": "No recent activity found."}},
{"structured_content": {"data": {"result": "No recent activity found."}}}
],
"is_error": false,
"structured_content_json": "{\"result\":\"No recent activity found.\"}"
}),
serde_json::json!({
"content": [{"text": {"text": "[]"}}],
"is_error": false
}),
] {
assert!(parse_drive_activity(output.to_string().as_bytes(), file_id)
.expect("empty activity response")
.is_empty());
}
}
#[test]
fn drive_activity_rejects_malformed_nonempty_responses() {
let file_id = "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456";
let output = serde_json::json!({
"content": [{"text": {"text": "unexpected nonempty response"}}],
"is_error": false
});
assert!(parse_drive_activity(output.to_string().as_bytes(), file_id).is_err());
}
#[test]
fn calendar_activity_serializes_provenance() {
let activity = CollectionCalendarActivity {
action_type: "comment".into(),
timestamp: "2026-08-27T18:05:03.000Z".into(),
actor_display_name: Some("Grace Hopper".into()),
actor_email: Some("grace@example.com".into()),
document_title: "Launch notes".into(),
document_url: "https://docs.google.com/document/d/1AbCdEfGhIjKlMnOpQrStUvWxYz_123456/edit"
.into(),
document_file_id: "1AbCdEfGhIjKlMnOpQrStUvWxYz_123456".into(),
source_calendar_url: "https://www.google.com/calendar/event?eid=calendar-source".into(),
source_attachment_url:
"https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz_123456/view".into(),
};
let value = serde_json::to_value(activity).expect("serialize activity");
assert_eq!(value["action_type"], "comment");
assert_eq!(
value["source_calendar_url"],
"https://www.google.com/calendar/event?eid=calendar-source"
);
assert!(value["source_attachment_url"]
.as_str()
.is_some_and(|url| url.contains("drive.google.com/file/d/")));
}
#[test]
fn drive_activity_window_is_bounded_by_valid_rfc3339_inputs() {
assert!(validate_activity_window("2026-08-26T00:00:00Z", Some("2026-08-27T00:00:00Z")).is_ok());
assert!(validate_activity_window("yesterday", None).is_err());
assert!(
validate_activity_window("2026-08-27T00:00:00Z", Some("2026-08-26T00:00:00Z")).is_err()
);
}
#[tokio::test]
async fn calendar_tool_output_is_bounded() {
let error = read_bounded(&b"12345"[..], 4)
.await
.expect_err("oversized output should fail");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
}

View File

@@ -0,0 +1,322 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import {
CalendarDays,
Ellipsis,
ExternalLink,
FileText,
GitBranch,
GitPullRequest,
Hash,
Link2,
ListTodo,
MessageSquare,
MessagesSquare,
StickyNote,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import type { DerivedCollectionActivity } from "../derivedLinks";
import { collectionReferenceIdentity } from "../memberDisplay";
import type { CollectionMember, CollectionReferenceType } from "../types";
export function DerivedCollectionActivityRow({
activity,
actorAvatarUrl,
actorLabel,
onOpenMessage,
onRemoveSource,
}: {
activity: DerivedCollectionActivity;
actorAvatarUrl?: string;
actorLabel?: string;
onOpenMessage: () => void;
onRemoveSource: () => Promise<void>;
}) {
const isMessage = activity.activityType === "channel-message";
const isDerived = !isMessage || activity.derivedFromSource === true;
const isDocumentActivity =
activity.activityType === "document-edit" ||
activity.activityType === "document-comment";
const isPullRequest = activity.activityType === "github-pr";
const isPullRequestActivity = activity.activityType === "github-pr-activity";
const identity = isMessage
? activity.eventId
: isDocumentActivity || isPullRequestActivity
? `${activity.url}:${activity.activityType}:${activity.createdAt}`
: activity.url;
const timestamp = activity.createdAt
? new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(activity.createdAt * 1_000))
: null;
const content = (
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<ActivityTypeGlyph activity={activity} />
{isDerived ? (
<span
className="text-muted-foreground"
title={`Derived from ${activity.provenanceLabel ?? "an explicit Collection source"}`}
>
<Link2
aria-label={`Derived from ${activity.provenanceLabel ?? "an explicit Collection source"}`}
className="h-3.5 w-3.5"
/>
</span>
) : null}
<span className="truncate text-sm font-medium">{activity.label}</span>
</div>
{isDocumentActivity ? (
<div className="mt-1 text-sm">
{activity.actorLabel}{" "}
{activity.activityType === "document-edit"
? "edited"
: "commented on"}{" "}
<span className="font-medium">{activity.label}</span>
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isPullRequestActivity ? (
<div className="mt-1 text-sm">
{activity.actorLabel ?? "Unknown GitHub actor"}{" "}
{activity.kind.replace(/^PR /u, "")}
{activity.stateLabel ? ` · ${activity.stateLabel}` : null}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isPullRequest ? (
<div className="mt-1 text-sm">
{activity.stateLabel ? (
<span className="font-medium capitalize">
{activity.stateLabel}
</span>
) : (
"Mentioned PR"
)}
{activity.actorLabel ? ` by ${activity.actorLabel}` : null}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isMessage ? (
<div className="mt-1 text-sm">
{actorLabel ?? "Unknown Buzz user"}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : timestamp ? (
<div className="mt-1 text-xs text-muted-foreground">{timestamp}</div>
) : null}
{activity.provenanceLabel ? (
<div className="mt-1 text-xs text-muted-foreground">
From {activity.provenanceLabel}
</div>
) : null}
{activity.url ? (
<div className="mt-1 truncate text-xs text-muted-foreground">
{activity.url}
</div>
) : null}
</div>
);
return (
<div
className="group flex items-center gap-3 p-4"
data-testid={`collection-derived-${encodeURIComponent(identity ?? "activity")}`}
>
{actorLabel ? (
<UserAvatar
avatarUrl={actorAvatarUrl ?? null}
displayName={actorLabel}
size="sm"
/>
) : null}
{isMessage ? (
<button
className="min-w-0 flex-1 text-left hover:underline"
onClick={onOpenMessage}
type="button"
>
{content}
</button>
) : (
content
)}
{!isMessage ? (
<Button
aria-label={`Open derived link ${activity.label}`}
onClick={() => {
void openUrl(activity.url).catch(() =>
toast.error("Failed to open link"),
);
}}
size="icon"
variant="ghost"
>
<ExternalLink className="h-4 w-4" />
</Button>
) : null}
<CollectionRowActions
derived={isDerived}
label={activity.label}
onRemove={onRemoveSource}
/>
</div>
);
}
function ActivityTypeGlyph({
activity,
}: {
activity: DerivedCollectionActivity;
}) {
const Icon =
activity.activityType === "github-pr" ||
activity.activityType === "github-pr-activity"
? GitPullRequest
: activity.activityType === "calendar-document"
? CalendarDays
: activity.activityType === "channel-message"
? activity.kind === "thread"
? MessagesSquare
: MessageSquare
: FileText;
return (
<Icon
aria-label={activity.kind}
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
);
}
export function CollectionMemberRow({
canOpen,
isRemoving,
member,
onOpen,
onRemove,
}: {
canOpen: boolean;
isRemoving: boolean;
member: CollectionMember;
onOpen: () => void;
onRemove: () => Promise<void>;
}) {
const identity = collectionReferenceIdentity(member.reference);
const label = member.label || identity;
const content = (
<div className="flex items-center gap-2">
<SourceTypeGlyph type={member.reference.type} />
<span className="truncate text-sm font-medium">{label}</span>
{member.reference.type === "external" ? (
<span className="rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">
{new URL(member.reference.url).hostname}
</span>
) : null}
</div>
);
return (
<div
className="group flex items-center gap-3 p-4"
data-testid={`collection-member-${member.id}`}
>
<div className="min-w-0 flex-1">
{canOpen && member.reference.type !== "external" ? (
<button
className="w-full text-left hover:underline"
onClick={onOpen}
type="button"
>
{content}
</button>
) : (
content
)}
<details className="mt-1 text-xs text-muted-foreground">
<summary className="w-fit cursor-pointer">Details</summary>
<code className="mt-1 block break-all">{identity}</code>
</details>
</div>
{member.reference.type === "external" ? (
<Button asChild size="icon" variant="ghost">
<a href={member.reference.url} rel="noreferrer" target="_blank">
<ExternalLink className="h-4 w-4" />
<span className="sr-only">Open external reference</span>
</a>
</Button>
) : null}
<CollectionRowActions
disabled={isRemoving}
label={member.label ?? member.reference.type}
onRemove={onRemove}
/>
</div>
);
}
function SourceTypeGlyph({ type }: { type: CollectionReferenceType }) {
const Icon =
type === "channel"
? Hash
: type === "repository"
? GitBranch
: type === "task"
? ListTodo
: type === "thread"
? MessagesSquare
: type === "message"
? MessageSquare
: type === "note"
? StickyNote
: ExternalLink;
return (
<Icon
aria-label={`${type} source`}
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
);
}
export function CollectionRowActions({
derived = false,
disabled = false,
label,
onRemove,
}: {
derived?: boolean;
disabled?: boolean;
label: string;
onRemove: () => Promise<void>;
}) {
const removeLabel = derived
? "Remove source from Collection"
: "Remove from Collection";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Actions for ${label}`}
className="opacity-60 transition-opacity hover:opacity-100 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
size="icon"
variant="ghost"
>
<Ellipsis className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={disabled}
onSelect={() => void onRemove()}
>
{removeLabel}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -3,20 +3,9 @@ import { openUrl } from "@tauri-apps/plugin-opener";
import {
ArrowLeft,
Bot,
CalendarDays,
Ellipsis,
ExternalLink,
FileText,
GitBranch,
GitPullRequest,
Hash,
Link2,
ListTodo,
MessageSquare,
MessagesSquare,
Plus,
Search,
StickyNote,
UserRound,
} from "lucide-react";
import * as React from "react";
@@ -24,6 +13,7 @@ import { toast } from "sonner";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import { truncatePubkey } from "@/shared/lib/pubkey";
import { useOpenEntityLink } from "@/shared/ui/markdown/entityLinks";
import { Button } from "@/shared/ui/button";
import {
@@ -53,17 +43,15 @@ import {
useCollectionQuery,
useDerivedCollectionLinks,
} from "../hooks";
import {
type DerivedCollectionActivity,
projectCollectionActivity,
} from "../derivedLinks";
import type {
CollectionMember,
CollectionReference,
CollectionReferenceType,
} from "../types";
import { projectCollectionActivity } from "../derivedLinks";
import type { CollectionReference, CollectionReferenceType } from "../types";
import { useCollectionScope } from "../useCollectionScope";
import { AddCollectionReferenceForm } from "./AddCollectionReferenceForm";
import {
CollectionMemberRow,
CollectionRowActions,
DerivedCollectionActivityRow,
} from "./CollectionDetailRows";
import { CollectionGlyph } from "./CollectionGlyph";
import { CollectionIconDialog } from "./CollectionIconDialog";
@@ -350,8 +338,8 @@ export function CollectionDetailScreen({
>
<TabsList aria-label="Collection sections">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="sources">Sources</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
</Tabs>
@@ -598,7 +586,7 @@ export function CollectionDetailScreen({
?.displayName ??
profiles[activity.authorPubkey.toLocaleLowerCase()]
?.name ??
activity.authorPubkey.slice(0, 8))
truncatePubkey(activity.authorPubkey))
: undefined)
}
key={`${activity.activityType}:${activity.eventId ?? activity.url}:${activity.createdAt}`}
@@ -738,297 +726,3 @@ export function CollectionDetailScreen({
</main>
);
}
function DerivedCollectionActivityRow({
activity,
actorAvatarUrl,
actorLabel,
onOpenMessage,
onRemoveSource,
}: {
activity: DerivedCollectionActivity;
actorAvatarUrl?: string;
actorLabel?: string;
onOpenMessage: () => void;
onRemoveSource: () => Promise<void>;
}) {
const isMessage = activity.activityType === "channel-message";
const isDerived = !isMessage || activity.derivedFromSource === true;
const isDocumentActivity =
activity.activityType === "document-edit" ||
activity.activityType === "document-comment";
const isPullRequest = activity.activityType === "github-pr";
const isPullRequestActivity = activity.activityType === "github-pr-activity";
const identity = isMessage
? activity.eventId
: isDocumentActivity || isPullRequestActivity
? `${activity.url}:${activity.activityType}:${activity.createdAt}`
: activity.url;
const timestamp = activity.createdAt
? new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(activity.createdAt * 1_000))
: null;
const content = (
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<ActivityTypeGlyph activity={activity} />
{isDerived ? (
<span
className="text-muted-foreground"
title={`Derived from ${activity.provenanceLabel ?? "an explicit Collection source"}`}
>
<Link2
aria-label={`Derived from ${activity.provenanceLabel ?? "an explicit Collection source"}`}
className="h-3.5 w-3.5"
/>
</span>
) : null}
<span className="truncate text-sm font-medium">{activity.label}</span>
</div>
{isDocumentActivity ? (
<div className="mt-1 text-sm">
{activity.actorLabel}{" "}
{activity.activityType === "document-edit"
? "edited"
: "commented on"}{" "}
<span className="font-medium">{activity.label}</span>
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isPullRequestActivity ? (
<div className="mt-1 text-sm">
{activity.actorLabel ?? "Unknown GitHub actor"}{" "}
{activity.kind.replace(/^PR /u, "")}
{activity.stateLabel ? ` · ${activity.stateLabel}` : null}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isPullRequest ? (
<div className="mt-1 text-sm">
{activity.stateLabel ? (
<span className="font-medium capitalize">
{activity.stateLabel}
</span>
) : (
"Mentioned PR"
)}
{activity.actorLabel ? ` by ${activity.actorLabel}` : null}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : isMessage ? (
<div className="mt-1 text-sm">
{actorLabel ?? "Unknown Buzz user"}
{timestamp ? ` · ${timestamp}` : null}
</div>
) : timestamp ? (
<div className="mt-1 text-xs text-muted-foreground">{timestamp}</div>
) : null}
{activity.provenanceLabel ? (
<div className="mt-1 text-xs text-muted-foreground">
From {activity.provenanceLabel}
</div>
) : null}
{activity.url ? (
<div className="mt-1 truncate text-xs text-muted-foreground">
{activity.url}
</div>
) : null}
</div>
);
return (
<div
className="group flex items-center gap-3 p-4"
data-testid={`collection-derived-${encodeURIComponent(identity ?? "activity")}`}
>
{actorLabel ? (
<UserAvatar
avatarUrl={actorAvatarUrl ?? null}
displayName={actorLabel}
size="sm"
/>
) : null}
{isMessage ? (
<button
className="min-w-0 flex-1 text-left hover:underline"
onClick={onOpenMessage}
type="button"
>
{content}
</button>
) : (
content
)}
{!isMessage ? (
<Button
aria-label={`Open derived link ${activity.label}`}
onClick={() => {
void openUrl(activity.url).catch(() =>
toast.error("Failed to open link"),
);
}}
size="icon"
variant="ghost"
>
<ExternalLink className="h-4 w-4" />
</Button>
) : null}
<CollectionRowActions
derived={isDerived}
label={activity.label}
onRemove={onRemoveSource}
/>
</div>
);
}
function ActivityTypeGlyph({
activity,
}: {
activity: DerivedCollectionActivity;
}) {
const Icon =
activity.activityType === "github-pr" ||
activity.activityType === "github-pr-activity"
? GitPullRequest
: activity.activityType === "calendar-document"
? CalendarDays
: activity.activityType === "channel-message"
? activity.kind === "thread"
? MessagesSquare
: MessageSquare
: FileText;
return (
<Icon
aria-label={activity.kind}
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
);
}
function CollectionMemberRow({
canOpen,
isRemoving,
member,
onOpen,
onRemove,
}: {
canOpen: boolean;
isRemoving: boolean;
member: CollectionMember;
onOpen: () => void;
onRemove: () => Promise<void>;
}) {
const identity = collectionReferenceIdentity(member.reference);
const label = member.label || identity;
const content = (
<div className="flex items-center gap-2">
<SourceTypeGlyph type={member.reference.type} />
<span className="truncate text-sm font-medium">{label}</span>
{member.reference.type === "external" ? (
<span className="rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">
{new URL(member.reference.url).hostname}
</span>
) : null}
</div>
);
return (
<div
className="group flex items-center gap-3 p-4"
data-testid={`collection-member-${member.id}`}
>
<div className="min-w-0 flex-1">
{canOpen && member.reference.type !== "external" ? (
<button
className="w-full text-left hover:underline"
onClick={onOpen}
type="button"
>
{content}
</button>
) : (
content
)}
<details className="mt-1 text-xs text-muted-foreground">
<summary className="w-fit cursor-pointer">Details</summary>
<code className="mt-1 block break-all">{identity}</code>
</details>
</div>
{member.reference.type === "external" ? (
<Button asChild size="icon" variant="ghost">
<a href={member.reference.url} rel="noreferrer" target="_blank">
<ExternalLink className="h-4 w-4" />
<span className="sr-only">Open external reference</span>
</a>
</Button>
) : null}
<CollectionRowActions
disabled={isRemoving}
label={member.label ?? member.reference.type}
onRemove={onRemove}
/>
</div>
);
}
function SourceTypeGlyph({ type }: { type: CollectionReferenceType }) {
const Icon =
type === "channel"
? Hash
: type === "repository"
? GitBranch
: type === "task"
? ListTodo
: type === "thread"
? MessagesSquare
: type === "message"
? MessageSquare
: type === "note"
? StickyNote
: ExternalLink;
return (
<Icon
aria-label={`${type} source`}
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
);
}
function CollectionRowActions({
derived = false,
disabled = false,
label,
onRemove,
}: {
derived?: boolean;
disabled?: boolean;
label: string;
onRemove: () => Promise<void>;
}) {
const removeLabel = derived
? "Remove source from Collection"
: "Remove from Collection";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Actions for ${label}`}
className="opacity-60 transition-opacity hover:opacity-100 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
size="icon"
variant="ghost"
>
<Ellipsis className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={disabled}
onSelect={() => void onRemove()}
>
{removeLabel}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}