From 22ba7a9004fb6bd56c05763cde1ecaa23e0c08e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 13:24:50 -0400 Subject: [PATCH] feat(documents): surface filesystem work-products in Document Center MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../crates/app_ui/src/app_root/onboarding.rs | 253 ++++++++++++++-- .../crates/app_ui/src/app_root/overview.rs | 272 ++++++++++++++---- .../crates/app_ui/src/app_root/sidebar.rs | 231 +++++++++++++-- AgentCompany-Rust/crates/app_ui/src/theme.rs | 104 +++++-- src/main/database/documents.ts | 28 ++ src/main/database/snapshot.ts | 11 + .../company-operations/DocumentCenter.tsx | 113 ++++++-- src/renderer/hooks/useCompanyController.ts | 1 + src/renderer/hooks/useCompanyData.ts | 3 + src/renderer/i18n/en.ts | 2 + src/renderer/i18n/zh.ts | 2 + .../views/sections/OperationsSections.tsx | 2 + src/shared/types/records-extended.ts | 20 ++ src/shared/types/snapshot.ts | 3 + tests/unit/store.test.ts | 3 + 15 files changed, 890 insertions(+), 158 deletions(-) diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs index df80b4fa..d7a766d3 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/onboarding.rs @@ -4,35 +4,234 @@ impl AppRoot { pub(super) fn render_onboarding(&self, cx: &mut Context) -> 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) +} diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs index 72d18ae0..81552fcf 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/overview.rs @@ -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::(); + 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)) +} diff --git a/AgentCompany-Rust/crates/app_ui/src/app_root/sidebar.rs b/AgentCompany-Rust/crates/app_ui/src/app_root/sidebar.rs index 8d56b52c..d7085cc0 100644 --- a/AgentCompany-Rust/crates/app_ui/src/app_root/sidebar.rs +++ b/AgentCompany-Rust/crates/app_ui/src/app_root/sidebar.rs @@ -3,65 +3,163 @@ use super::*; impl AppRoot { pub(super) fn render_sidebar(&self, cx: &mut Context) -> 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( + label: &'static str, + active: bool, + cx: &mut Context, + on_click: impl Fn(&mut V, &mut Context) + '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) +} diff --git a/AgentCompany-Rust/crates/app_ui/src/theme.rs b/AgentCompany-Rust/crates/app_ui/src/theme.rs index 4114342e..d5aaf760 100644 --- a/AgentCompany-Rust/crates/app_ui/src/theme.rs +++ b/AgentCompany-Rust/crates/app_ui/src/theme.rs @@ -1,81 +1,145 @@ use gpui::{Hsla, rgb}; +pub fn window_bg() -> Hsla { + rgb(0xf7f8fb).into() +} + pub fn canvas() -> Hsla { - rgb(0xf3efe8).into() + rgb(0xf7f8fb).into() } pub fn shell_header() -> Hsla { - rgb(0xf8f2e9).into() + rgb(0xffffff).into() } pub fn panel() -> Hsla { - rgb(0xfffbf5).into() + rgb(0xffffff).into() } pub fn panel_border() -> Hsla { - rgb(0xd9cdbf).into() + rgb(0xe7eaf0).into() } pub fn panel_hover() -> Hsla { - rgb(0xf4ede4).into() + rgb(0xf3f6fb).into() } pub fn panel_inset() -> Hsla { - rgb(0xf7f0e7).into() + rgb(0xf5f7fb).into() } pub fn panel_inset_border() -> Hsla { - rgb(0xeee3d7).into() + rgb(0xe8ecf3).into() +} + +pub fn panel_muted() -> Hsla { + rgb(0xf1f4f8).into() +} + +pub fn split_divider() -> Hsla { + rgb(0xdbe2ea).into() } pub fn text_primary() -> Hsla { - rgb(0x171411).into() + rgb(0x171c25).into() +} + +pub fn text_secondary() -> Hsla { + rgb(0x3e4757).into() } pub fn text_muted() -> Hsla { - rgb(0x6f665d).into() + rgb(0x6e7686).into() +} + +pub fn text_subtle() -> Hsla { + rgb(0x8b93a1).into() } pub fn primary_action() -> Hsla { - rgb(0x9e3d2d).into() + rgb(0x556f90).into() } pub fn primary_action_hover() -> Hsla { - rgb(0xb24f3e).into() + rgb(0x617b9c).into() } pub fn primary_action_text() -> Hsla { - rgb(0xfff7f0).into() + rgb(0xfafcff).into() +} + +pub fn accent_blue() -> Hsla { + rgb(0x5876a3).into() +} + +pub fn accent_blue_soft() -> Hsla { + rgb(0xebf1ff).into() +} + +pub fn accent_teal() -> Hsla { + rgb(0x167f76).into() +} + +pub fn accent_teal_soft() -> Hsla { + rgb(0xe9faf4).into() +} + +pub fn accent_orange() -> Hsla { + rgb(0xd97931).into() +} + +pub fn accent_orange_soft() -> Hsla { + rgb(0xfdebdc).into() +} + +pub fn danger() -> Hsla { + rgb(0xc94646).into() } pub fn danger_bg() -> Hsla { - rgb(0xffefe8).into() + rgb(0xfdeaea).into() +} + +pub fn success_bg() -> Hsla { + rgb(0xebfbf3).into() } pub fn sidebar_bg() -> Hsla { - rgb(0x161514).into() + rgb(0xffffff).into() } pub fn sidebar_surface() -> Hsla { - rgb(0x211d1a).into() + rgb(0xffffff).into() } pub fn sidebar_hover() -> Hsla { - rgb(0x2c2825).into() + rgb(0xf4f6fa).into() } pub fn sidebar_surface_hover() -> Hsla { - rgb(0x362c27).into() + rgb(0xf4f6fa).into() } pub fn sidebar_divider() -> Hsla { - rgb(0x2f2924).into() + rgb(0xe7eaf0).into() } pub fn sidebar_text() -> Hsla { - rgb(0xf5eee4).into() + rgb(0x596273).into() +} + +pub fn sidebar_text_active() -> Hsla { + rgb(0x111827).into() } pub fn selection_fill() -> Hsla { - rgb(0xefe2d4).into() + rgb(0xebf1ff).into() +} + +pub fn selection_border() -> Hsla { + rgb(0xc8d8f0).into() +} + +pub fn badge_dark() -> Hsla { + rgb(0x6b7280).into() } diff --git a/src/main/database/documents.ts b/src/main/database/documents.ts index 1574a72e..b0e92091 100644 --- a/src/main/database/documents.ts +++ b/src/main/database/documents.ts @@ -299,6 +299,34 @@ export function buildDocumentMethods( })); }, + /** + * Company-scoped work-product listing, newest first. Used by the + * renderer snapshot so the DocumentCenter can surface filesystem + * artifacts (type = "file") the agents produce via bash/write/edit + * — these are the deliverables operators expect to see alongside + * prose documents, so the page stops looking empty when every + * artifact of the night lives in `work_products`. + */ + listWorkProductsForCompany(companyId: string, limit = 500) { + return readAll<{ + id: string; company_id: string; project_id: string | null; task_id: string | null; + type: string; provider: string; external_id: string | null; title: string; + url: string | null; status: string; review_state: string; is_primary: number; + summary: string | null; metadata_json: string | null; created_by_run_id: string | null; + created_at: string; updated_at: string; + }>( + "SELECT * FROM work_products WHERE company_id = ? ORDER BY updated_at DESC LIMIT ?", + companyId, limit, + ).map((r) => ({ + id: r.id, companyId: r.company_id, projectId: r.project_id, taskId: r.task_id, + type: r.type, provider: r.provider, externalId: r.external_id, title: r.title, + url: r.url, status: r.status, reviewState: r.review_state, + isPrimary: Boolean(r.is_primary), summary: r.summary, + metadataJson: r.metadata_json, createdByRunId: r.created_by_run_id, + createdAt: r.created_at, updatedAt: r.updated_at, + })); + }, + deleteWorkProduct(id: string, companyId: string): number { const result = getDb() .prepare("DELETE FROM work_products WHERE id = ? AND company_id = ?") diff --git a/src/main/database/snapshot.ts b/src/main/database/snapshot.ts index 238587bd..19e3c215 100644 --- a/src/main/database/snapshot.ts +++ b/src/main/database/snapshot.ts @@ -129,6 +129,17 @@ export function buildSnapshotMethods( goalId: r.goal_id, parentDocId: r.parent_doc_id, version: r.version, status: r.status, tagsJson: r.tags_json, latestRevisionId: r.latest_revision_id ?? null, createdAt: r.created_at, updatedAt: r.updated_at, })), + workProducts: readAll( + `select id, company_id, project_id, task_id, type, provider, external_id, title, url, status, review_state, is_primary, summary, metadata_json, created_by_run_id, created_at, updated_at from work_products${companyWhere} order by updated_at desc limit 500`, + ...companyArgs, + ).map((r) => ({ + id: r.id, companyId: r.company_id, projectId: r.project_id, taskId: r.task_id, + type: r.type, provider: r.provider, externalId: r.external_id, title: r.title, + url: r.url, status: r.status, reviewState: r.review_state, + isPrimary: Boolean(r.is_primary), summary: r.summary, + metadataJson: r.metadata_json, createdByRunId: r.created_by_run_id, + createdAt: r.created_at, updatedAt: r.updated_at, + })), knowledgeBase: readAll( `select id, company_id, category, topic, content, author_agent_id, importance, tags_json, referenced_entity_type, referenced_entity_id, created_at, updated_at from knowledge_base${companyWhere} order by updated_at desc`, ...companyArgs, diff --git a/src/renderer/components/company-operations/DocumentCenter.tsx b/src/renderer/components/company-operations/DocumentCenter.tsx index bf4d5023..ba74f8aa 100644 --- a/src/renderer/components/company-operations/DocumentCenter.tsx +++ b/src/renderer/components/company-operations/DocumentCenter.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { FileText, FloppyDisk, NotePencil, Trash } from "../ui/icons"; -import type { AgentRecord, DocumentRecord, GoalRecord, ProjectRecord } from "@shared/types"; +import type { AgentRecord, DocumentRecord, GoalRecord, ProjectRecord, WorkProductRecord } from "@shared/types"; import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "../ui"; import { useT } from "../../i18n"; import { documentTypeOptions, documentStatusOptions, splitTags, parseTags } from "./helpers"; @@ -65,38 +65,101 @@ function DocumentEditor({ companyId, agents, projects, goals, document, onSaved ); } -export function DocumentCenter({ companyId, documents, agents, projects, goals, onRefresh }: { - companyId: string; documents: DocumentRecord[]; agents: AgentRecord[]; projects: ProjectRecord[]; - goals: GoalRecord[]; onRefresh: () => Promise; -}) { +function WorkProductRow({ product }: { product: WorkProductRecord }) { + const subtitle = [ + product.type.replaceAll("_", " "), + product.provider, + product.externalId ?? product.url ?? null, + product.updatedAt.slice(0, 10), + ].filter((v): v is string => Boolean(v)).join(" · "); + const primaryAction = product.url + ? () => { void window.agentCompany.openPath(product.url!); } + : product.provider === "filesystem" && product.externalId + ? () => { void window.agentCompany.openPath(product.externalId!); } + : null; + const rowBody = ( +
+
+ + {product.title} +
+
{subtitle}
+
+ ); + return ( + +
+ {primaryAction + ? + : rowBody} +
+ +
+
+
+ ); +} + +interface DocumentCenterProps { + companyId: string; + documents: DocumentRecord[]; + workProducts: WorkProductRecord[]; + agents: AgentRecord[]; + projects: ProjectRecord[]; + goals: GoalRecord[]; + onRefresh: () => Promise; +} + +export function DocumentCenter({ companyId, documents, workProducts, agents, projects, goals, onRefresh }: DocumentCenterProps) { const t = useT(); const [editingId, setEditingId] = useState(null); const [deleteId, setDeleteId] = useState(null); const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]); + const totalCount = documents.length + workProducts.length; + return (
- setEditingId(NEW_ENTITY_ID)} /> - - {documents.map((document) => ( - -
- +
+ + + +
-
{document.type.replaceAll("_", " ")} · v{document.version} · {document.updatedAt.slice(0, 10)}
- -
- - - -
-
- - ))} - + + ))} + + + ) : null} + {workProducts.length > 0 ? ( +
+
+ {t("ops.documentsWorkProducts")} +
+ + {workProducts.map((product) => ( + + ))} + +
+ ) : null} setEditingId(null)} width="760px"> { setEditingId(null); await onRefresh(); }} /> diff --git a/src/renderer/hooks/useCompanyController.ts b/src/renderer/hooks/useCompanyController.ts index fd4ef9b0..4c657e3e 100644 --- a/src/renderer/hooks/useCompanyController.ts +++ b/src/renderer/hooks/useCompanyController.ts @@ -172,6 +172,7 @@ export function useCompanyController() { filteredActivity: companyData.filteredActivity, companySecrets: companyData.companySecrets, companyDocuments: companyData.companyDocuments, + companyWorkProducts: companyData.companyWorkProducts, companyKnowledge: companyData.companyKnowledge, companyMeetings: companyData.companyMeetings, companySprints: companyData.companySprints, diff --git a/src/renderer/hooks/useCompanyData.ts b/src/renderer/hooks/useCompanyData.ts index 1f649801..d836ad22 100644 --- a/src/renderer/hooks/useCompanyData.ts +++ b/src/renderer/hooks/useCompanyData.ts @@ -37,6 +37,7 @@ export function useCompanyData( const filteredActivity = useMemo(() => filterByCompany(snapshot?.activity, currentCompany), [snapshot, currentCompany]); const companySecrets = useMemo(() => filterByCompany(snapshot?.secrets, currentCompany), [snapshot, currentCompany]); const companyDocuments = useMemo(() => filterByCompany(snapshot?.documents, currentCompany), [snapshot, currentCompany]); + const companyWorkProducts = useMemo(() => filterByCompany(snapshot?.workProducts, currentCompany), [snapshot, currentCompany]); const companyKnowledge = useMemo(() => filterByCompany(snapshot?.knowledgeBase, currentCompany), [snapshot, currentCompany]); const companyMeetings = useMemo(() => filterByCompany(snapshot?.meetings, currentCompany), [snapshot, currentCompany]); const companySprints = useMemo(() => filterByCompany(snapshot?.sprints, currentCompany), [snapshot, currentCompany]); @@ -109,6 +110,7 @@ export function useCompanyData( filteredActivity, companySecrets, companyDocuments, + companyWorkProducts, companyKnowledge, companyMeetings, companySprints, @@ -134,6 +136,7 @@ export function useCompanyData( filteredActivity, companySecrets, companyDocuments, + companyWorkProducts, companyKnowledge, companyMeetings, companySprints, diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index 233081a9..4d1dbfaf 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -871,6 +871,8 @@ export const en: Record = { // ── Company Operations ── "ops.documentCenter": "Document Center", + "ops.documentsProse": "Prose documents", + "ops.documentsWorkProducts": "Files & deliverables", "ops.knowledgeBase": "Knowledge Base", "ops.meetings": "Meetings", "ops.sprintBoard": "Sprint Board", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index bf39cb75..85a1a47d 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -871,6 +871,8 @@ export const zh: Record = { // ── 公司运营 ── "ops.documentCenter": "文档中心", + "ops.documentsProse": "文稿", + "ops.documentsWorkProducts": "文件与交付物", "ops.knowledgeBase": "知识库", "ops.meetings": "会议", "ops.sprintBoard": "迭代看板", diff --git a/src/renderer/views/sections/OperationsSections.tsx b/src/renderer/views/sections/OperationsSections.tsx index bb919865..12cb254d 100644 --- a/src/renderer/views/sections/OperationsSections.tsx +++ b/src/renderer/views/sections/OperationsSections.tsx @@ -38,6 +38,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) { companyProjects, companyWorkspaces, companyDocuments, + companyWorkProducts, companyKnowledge, companyMeetings, companySprints, @@ -215,6 +216,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) { { socialAccounts: [], meetings: [], documents: [], + workProducts: [], knowledgeBase: [], sprints: [], automationRules: [], @@ -174,6 +175,7 @@ describe("useAppStore", () => { socialAccounts: [], meetings: [], documents: [], + workProducts: [], knowledgeBase: [], sprints: [], automationRules: [], @@ -247,6 +249,7 @@ function makeSnapshot(overrides: Partial = {}): ProfileSnapshot socialAccounts: [], meetings: [], documents: [], + workProducts: [], knowledgeBase: [], sprints: [], automationRules: [],