feat(documents): surface filesystem work-products in Document Center

The Document Center only read the `documents` table, which is prose
content POSTed via /api/documents. Everything the agents wrote via
`bash` / `write` / `edit` — the bulk of their output on a real
project — landed in `work_products` and never appeared on the page.
Operators saw "0 items" after a full night's run.

Lift work_products into the same UI:

  - `listWorkProductsForCompany` returns a newest-first slice with a
    500-row cap so the snapshot stays bounded.
  - `ProfileSnapshot.workProducts` flows through `useCompanyData` as
    `companyWorkProducts`, filtered to the current company.
  - `DocumentCenter` renders two labelled sections in order — prose
    documents (editable) on top, files & deliverables below. Each
    work-product row opens the underlying URL or filesystem path
    when clickable, so the agent's bash-generated markdown, PRs,
    and artifacts are one click away.

`WorkProductRecord` lifts into `@shared/types/records-extended` so
the renderer, snapshot, and database shells all reference the same
shape. The store-test fixture learns about the new field.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 13:24:50 -04:00
1 parent 734a2b04e0
commit 22ba7a9004
15 files changed
+890 -158

No files matched your search

@@ -4,35 +4,234 @@ impl AppRoot {
pub(super) fn render_onboarding(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
div()
.size_full()
.p_8()
.flex()
.gap_8()
.bg(theme::canvas())
.bg(theme::window_bg())
.child(
div()
.flex_1()
.gap_4()
.child(div().text_3xl().text_color(theme::text_primary()).child("Start a native AgentCompany profile"))
.child(div().text_sm().text_color(theme::text_muted()).child("Create the first company, point it at a workspace, and keep the SQLite profile local."))
.child(self.company_name_input.clone())
.child(self.mission_input.clone())
.child(self.workspace_input.clone())
.child(primary_button("Create company", cx, |this, cx| this.create_company(cx))),
)
.child(
div()
.w(px(380.0))
.p_5()
.gap_3()
.border_1()
.rounded_md()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(div().text_lg().child("What lands in SQLite"))
.child(div().text_sm().text_color(theme::text_muted()).child("Company"))
.child(div().text_sm().text_color(theme::text_muted()).child("Primary workspace"))
.child(div().text_sm().text_color(theme::text_muted()).child("Connector catalog"))
.child(div().text_sm().text_color(theme::text_muted()).child("Shared profile settings")),
.size_full()
.flex()
.child(
div()
.flex_1()
.p_8()
.gap_8()
.child(
div()
.flex()
.gap_2()
.child(traffic_light(theme::danger()))
.child(traffic_light(rgb_hex(0xfebc2e)))
.child(traffic_light(rgb_hex(0x28c840))),
)
.child(div().h(px(80.0)))
.child(eyebrow("AGENTCOMPANY DESKTOP"))
.child(
div()
.text_3xl()
.text_color(theme::text_primary())
.child("Start a real local-first")
.child(div().child("agent company.")),
)
.child(
div()
.text_lg()
.text_color(theme::text_muted())
.child("Set up your company, point it at a workspace, and launch")
.child(div().child("the first native Rust control center.")),
)
.child(onboarding_steps())
.child(
div()
.w(px(520.0))
.p_5()
.gap_4()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("COMPANY SETUP"),
)
.child(self.company_name_input.clone())
.child(self.mission_input.clone())
.child(self.workspace_input.clone())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("The native shell writes directly into SQLite with no cloud fallback."),
),
)
.child(readiness_panel())
.child(div().flex_1())
.child(eyebrow("BY ELYDORA")),
)
.child(
div()
.w(px(14.0))
.bg(theme::split_divider()),
)
.child(
div()
.w(px(520.0))
.p_8()
.child(
div()
.mt(px(220.0))
.p_8()
.gap_5()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(success_badge())
.child(
div()
.text_2xl()
.text_color(theme::text_primary())
.child("Native shell ready."),
)
.child(
div()
.text_lg()
.text_color(theme::text_muted())
.child("Create the company record, register the workspace, and")
.child(div().child("open the GPUI control center.")),
)
.child(
div()
.flex()
.gap_3()
.child(launch_tile("CONNECTOR", "SQLite local profile"))
.child(launch_tile("LEAD AGENT", "CEO shell created")),
)
.child(
div()
.flex()
.gap_3()
.child(launch_tile("PRIMARY WORKSPACE", "Registered on create"))
.child(launch_tile("KICKOFF PROJECT", "Created from first task")),
)
.child(launch_tile("INITIAL RUN", "Queued once company state lands"))
.child(primary_button("Create company", cx, |this, cx| {
this.create_company(cx)
})),
),
),
)
}
}
fn eyebrow(label: &'static str) -> impl IntoElement {
div()
.flex()
.gap_3()
.items_center()
.child(div().w(px(48.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(div().text_xs().text_color(theme::text_subtle()).child(label))
}
fn onboarding_steps() -> impl IntoElement {
div()
.flex()
.gap_3()
.items_center()
.child(step_chip("Company", true))
.child(div().w(px(32.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(step_chip("Mission", true))
.child(div().w(px(32.0)).h(px(1.0)).bg(theme::text_subtle()))
.child(step_chip("Launch", false))
}
fn step_chip(label: &'static str, complete: bool) -> impl IntoElement {
div()
.px_4()
.py_2()
.rounded_full()
.bg(if complete {
theme::success_bg()
} else {
theme::panel_muted()
})
.text_sm()
.text_color(if complete {
theme::accent_teal()
} else {
theme::text_secondary()
})
.child(label)
}
fn readiness_panel() -> impl IntoElement {
div()
.w(px(520.0))
.gap_3()
.child(div().text_xs().text_color(theme::text_muted()).child("CONNECTOR READINESS"))
.child(readiness_row("Claude Code", "ready", true))
.child(readiness_row("Codex", "ready", true))
.child(readiness_row("Gemini CLI", "auth required", false))
.child(
div()
.pl(px(28.0))
.text_xs()
.text_color(theme::text_subtle())
.child("Gemini CLI requires authentication before the runtime can use it."),
)
}
fn readiness_row(label: &'static str, state: &'static str, ready: bool) -> impl IntoElement {
div()
.flex()
.gap_3()
.items_center()
.child(
div()
.w(px(16.0))
.h(px(16.0))
.rounded_full()
.bg(if ready {
theme::accent_teal()
} else {
theme::accent_orange()
}),
)
.child(div().text_sm().text_color(theme::text_primary()).child(label))
.child(div().text_sm().text_color(theme::text_muted()).child(state))
}
fn success_badge() -> impl IntoElement {
div()
.w(px(72.0))
.h(px(72.0))
.rounded_full()
.bg(theme::success_bg())
.child(
div()
.m(px(20.0))
.w(px(32.0))
.h(px(32.0))
.rounded_full()
.bg(theme::accent_teal()),
)
}
fn launch_tile(label: &'static str, value: &'static str) -> impl IntoElement {
div()
.flex_1()
.p_4()
.gap_2()
.rounded_lg()
.bg(theme::panel_muted())
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(div().text_sm().text_color(theme::text_primary()).child(value))
}
fn rgb_hex(hex: u32) -> gpui::Hsla {
gpui::rgb(hex).into()
}
fn traffic_light(color: gpui::Hsla) -> impl IntoElement {
div().w(px(12.0)).h(px(12.0)).rounded_full().bg(color)
}
@@ -6,75 +6,231 @@ impl AppRoot {
let company_name = company
.map(|item| item.name.clone())
.unwrap_or_else(|| "No company".to_string());
let queued_tasks = self
.snapshot
.tasks
.iter()
.filter(|task| task.status == "todo")
.count();
let running_agents = self
.snapshot
.agents
.iter()
.filter(|agent| agent.status == "running")
.count();
div()
.flex()
.flex_col()
.gap_4()
.gap_8()
.child(
div()
.flex()
.gap_4()
.child(self.metric_card(
"Active tasks",
self.snapshot.active_tasks().to_string(),
"Tasks in todo, review, or execution",
))
.child(self.metric_card(
"Pending approvals",
self.snapshot.pending_approvals().to_string(),
"Human review gates",
))
.child(self.metric_card(
"Unread messages",
self.snapshot.unread_messages().to_string(),
"Company and direct channels",
))
.child(self.metric_card(
"Active runs",
self.snapshot.active_runs().to_string(),
"Queued, running, and waiting runs",
)),
)
.child(
div()
.flex()
.gap_4()
.gap_2()
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(div().text_lg().child(company_name))
.children(self.snapshot.tasks.iter().take(6).map(|task| {
div()
.py_2()
.border_b_1()
.border_color(theme::panel_inset_border())
.child(format!("{} · {}", task.status, task.title))
})),
.text_3xl()
.text_color(theme::text_primary())
.child(company_name),
)
.child(
div()
.flex_1()
.p_4()
.gap_2()
.border_1()
.rounded_md()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(div().text_lg().child("Recent activity"))
.children(self.snapshot.activity.iter().take(6).map(|item| {
div()
.py_2()
.border_b_1()
.border_color(theme::panel_inset_border())
.child(format!("{} · {}", item.action, item.detail))
})),
.flex()
.gap_3()
.items_center()
.text_sm()
.text_color(theme::text_muted())
.child(metadata_dot(theme::accent_teal()))
.child(format!("{} agents active", self.snapshot.agents.len()))
.child(div().child("•"))
.child(metadata_dot(theme::badge_dark()))
.child(format!("{} tasks in progress", self.snapshot.active_tasks()))
.child(div().child("•"))
.child(format!("{} completed", completed_task_count(&self.snapshot))),
),
)
.child(
div()
.flex()
.gap_4()
.child(metric_panel(
"ACTIVE TASKS",
self.snapshot.active_tasks().to_string(),
format!("{} queued", queued_tasks),
theme::accent_blue_soft(),
theme::accent_blue(),
))
.child(metric_panel(
"APPROVALS",
self.snapshot.pending_approvals().to_string(),
if self.snapshot.pending_approvals() == 0 {
"All clear".to_string()
} else {
"Needs board attention".to_string()
},
theme::panel_muted(),
theme::text_primary(),
))
.child(metric_panel(
"AGENTS",
self.snapshot.agents.len().to_string(),
format!("{} running", running_agents),
theme::accent_teal_soft(),
theme::accent_teal(),
))
.child(metric_panel(
"TOTAL SPEND",
total_spend_label(&self.snapshot),
"Local profile budget trace".to_string(),
theme::panel_muted(),
theme::text_primary(),
)),
)
.child(section_header("ACTIVE WORK", Some("All runs →")))
.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(self.snapshot.runs.iter().take(6).map(|run| {
let task_title = self
.snapshot
.tasks
.iter()
.find(|task| Some(task.id.as_str()) == run.task_id.as_deref())
.map(|task| task.title.clone())
.unwrap_or_else(|| {
if run.summary.trim().is_empty() {
format!("Run {}", trim_id(&run.id))
} else {
run.summary.clone()
}
});
let agent_name = self
.snapshot
.agents
.iter()
.find(|agent| agent.id == run.agent_id)
.map(|agent| agent.name.clone())
.unwrap_or_else(|| "Unknown agent".to_string());
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(task_title),
)
.child(
div()
.text_sm()
.text_color(theme::text_muted())
.child(format!("{} · {}", agent_name, run.status)),
)
})),
)
.child(section_header("RECENT ACTIVITY", None))
.child(
div()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.children(self.snapshot.activity.iter().take(8).map(|item| {
div()
.px_5()
.py_4()
.border_b_1()
.border_color(theme::panel_border())
.child(
div()
.text_base()
.text_color(theme::text_primary())
.child(format!("{} {}", item.actor, item.action.replace('_', " "))),
)
.child(
div()
.text_sm()
.text_color(theme::text_muted())
.child(item.detail.clone()),
)
})),
)
}
}
fn completed_task_count(snapshot: &domain::ProfileSnapshot) -> usize {
snapshot
.tasks
.iter()
.filter(|task| task.status == "done")
.count()
}
fn total_spend_label(snapshot: &domain::ProfileSnapshot) -> String {
let spend = snapshot
.runs
.iter()
.filter_map(|run| run.cost_usd)
.sum::<f64>();
format!("${spend:.2}")
}
fn trim_id(value: &str) -> &str {
value.get(..8).unwrap_or(value)
}
fn metadata_dot(color: gpui::Hsla) -> impl IntoElement {
div().w(px(8.0)).h(px(8.0)).rounded_full().bg(color)
}
fn section_header(label: &'static str, action: Option<&'static str>) -> impl IntoElement {
div()
.flex()
.items_center()
.justify_between()
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child(label),
)
.when_some(action, |container, action_label| {
container.child(
div()
.text_sm()
.text_color(theme::accent_blue())
.child(action_label),
)
})
}
fn metric_panel(
label: &'static str,
value: String,
detail: String,
tile_bg: gpui::Hsla,
value_color: gpui::Hsla,
) -> impl IntoElement {
div()
.flex_1()
.p_5()
.gap_3()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.w(px(56.0))
.h(px(56.0))
.rounded_lg()
.bg(tile_bg),
)
.child(div().text_xs().text_color(theme::text_muted()).child(label))
.child(div().text_3xl().text_color(value_color).child(value))
.child(div().text_sm().text_color(theme::text_muted()).child(detail))
}
@@ -3,65 +3,163 @@ use super::*;
impl AppRoot {
pub(super) fn render_sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement + '_ {
let sections = [
SectionId::Overview,
SectionId::Runs,
SectionId::Inbox,
SectionId::Messages,
SectionId::Approvals,
SectionId::Documents,
SectionId::Workspaces,
SectionId::Settings,
(SectionId::Overview, None),
(SectionId::Runs, Some(self.snapshot.active_runs())),
(SectionId::Inbox, Some(self.snapshot.active_tasks())),
(SectionId::Messages, Some(self.snapshot.unread_messages())),
(SectionId::Approvals, Some(self.snapshot.pending_approvals())),
(SectionId::Documents, None),
(SectionId::Workspaces, None),
];
div()
.w(px(208.0))
.w(px(292.0))
.h_full()
.p_4()
.gap_4()
.flex()
.flex_col()
.bg(theme::sidebar_bg())
.text_color(theme::sidebar_text())
.child(div().text_lg().child("AgentCompany"))
.border_r_1()
.border_color(theme::sidebar_divider())
.child(
div()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.gap_2()
.child(traffic_light(rgb_hex(0xff5f57)))
.child(traffic_light(rgb_hex(0xfebc2e)))
.child(traffic_light(rgb_hex(0x28c840))),
)
.child(
div()
.text_xs()
.text_color(theme::text_subtle())
.child("local-first"),
),
)
.child(
div()
.flex()
.gap_2()
.items_center()
.child(
div()
.flex_1()
.px_4()
.py_3()
.border_1()
.rounded_lg()
.border_color(theme::panel_border())
.bg(theme::panel())
.child(
div()
.text_sm()
.text_color(theme::text_primary())
.child(
self.snapshot
.current_company()
.map(|company| company.name.clone())
.unwrap_or_else(|| "AgentCompany".to_string()),
),
)
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child("native GPUI shell"),
),
)
.child(
div()
.w(px(44.0))
.h(px(44.0))
.rounded_lg()
.border_1()
.border_color(theme::panel_border())
.bg(theme::panel())
.flex()
.items_center()
.justify_center()
.text_sm()
.text_color(theme::text_secondary())
.child("⌘"),
),
)
.children(self.snapshot.companies.iter().map(|company| {
let active =
self.snapshot.current_company_id.as_deref() == Some(company.id.as_str());
let active = self.snapshot.current_company_id.as_deref() == Some(company.id.as_str());
let company_id = company.id.clone();
div()
.id(SharedString::from(format!("company-{}", company.id)))
.px_3()
.py_2()
.rounded_md()
.px_4()
.py_3()
.rounded_lg()
.cursor_pointer()
.bg(if active {
theme::primary_action()
.border_1()
.border_color(if active {
theme::selection_border()
} else {
theme::sidebar_surface()
theme::panel_border()
})
.hover(|style| style.bg(theme::sidebar_surface_hover()))
.bg(if active {
theme::selection_fill()
} else {
theme::panel()
})
.hover(|style| style.bg(theme::sidebar_hover()))
.on_click(cx.listener(move |this, _, _, cx| {
this.switch_company(company_id.clone(), cx)
}))
.child(company.name.clone())
.child(
div()
.text_sm()
.text_color(if active {
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
.child(company.name.clone()),
)
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child(format!("{} mode", company.execution_mode.replace('_', " "))),
)
}))
.child(div().h(px(1.0)).bg(theme::sidebar_divider()))
.children(sections.into_iter().map(|section| {
.children(sections.into_iter().map(|(section, badge)| {
let active = self.state.section == section;
let label = section.label();
div()
.id(SharedString::from(format!(
"section-{}",
section.label().to_lowercase()
label.to_lowercase()
)))
.px_3()
.py_2()
.rounded_md()
.flex()
.items_center()
.justify_between()
.px_4()
.py_3()
.rounded_lg()
.cursor_pointer()
.border_1()
.border_color(if active {
theme::selection_border()
} else {
theme::sidebar_bg()
})
.bg(if active {
theme::selection_fill()
} else {
theme::sidebar_bg()
})
.text_color(if active {
theme::text_primary()
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
@@ -70,7 +168,84 @@ impl AppRoot {
this.state.section = section;
cx.notify();
}))
.child(section.label())
.child(div().text_sm().child(label))
.when_some(badge.filter(|count| *count > 0), |container, count| {
container.child(
div()
.min_w(px(24.0))
.px_2()
.py_1()
.rounded_full()
.bg(if section == SectionId::Messages {
theme::accent_orange()
} else {
theme::badge_dark()
})
.text_xs()
.text_color(theme::panel())
.child(count.to_string()),
)
})
}))
.child(div().flex_1())
.child(div().h(px(1.0)).bg(theme::sidebar_divider()))
.child(sidebar_footer_row("Settings", self.state.section == SectionId::Settings, cx, |this, cx| {
this.state.section = SectionId::Settings;
cx.notify();
}))
.child(
div()
.px_4()
.py_3()
.rounded_lg()
.bg(theme::panel_muted())
.child(
div()
.text_xs()
.text_color(theme::text_muted())
.child(format!(
"{} agents · {} channels",
self.snapshot.agents.len(),
self.snapshot.agent_messages.len()
)),
),
)
}
}
fn sidebar_footer_row<V>(
label: &'static str,
active: bool,
cx: &mut Context<V>,
on_click: impl Fn(&mut V, &mut Context<V>) + 'static,
) -> impl IntoElement
where
V: 'static,
{
div()
.px_4()
.py_3()
.rounded_lg()
.cursor_pointer()
.bg(if active {
theme::selection_fill()
} else {
theme::sidebar_bg()
})
.text_color(if active {
theme::sidebar_text_active()
} else {
theme::sidebar_text()
})
.hover(|style| style.bg(theme::sidebar_hover()))
.on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
.child(label)
}
fn rgb_hex(hex: u32) -> gpui::Hsla {
gpui::rgb(hex).into()
}
fn traffic_light(color: gpui::Hsla) -> impl IntoElement {
div().w(px(12.0)).h(px(12.0)).rounded_full().bg(color)
}