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:
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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 = ?")
|
||||
|
||||
@@ -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<SqliteRow>(
|
||||
`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<SqliteRow>(
|
||||
`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,
|
||||
|
||||
@@ -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<void>;
|
||||
}) {
|
||||
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 = (
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
|
||||
<FileText size={15} className="text-muted-foreground" />
|
||||
<span className="truncate">{product.title}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<ListRow>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
{primaryAction
|
||||
? <button type="button" className="min-w-0 flex-1 text-left" onClick={primaryAction}>{rowBody}</button>
|
||||
: rowBody}
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusPill status={product.status} />
|
||||
</div>
|
||||
</div>
|
||||
</ListRow>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocumentCenterProps {
|
||||
companyId: string;
|
||||
documents: DocumentRecord[];
|
||||
workProducts: WorkProductRecord[];
|
||||
agents: AgentRecord[];
|
||||
projects: ProjectRecord[];
|
||||
goals: GoalRecord[];
|
||||
onRefresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function DocumentCenter({ companyId, documents, workProducts, agents, projects, goals, onRefresh }: DocumentCenterProps) {
|
||||
const t = useT();
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
|
||||
|
||||
const totalCount = documents.length + workProducts.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
<ListFrame>
|
||||
{documents.map((document) => (
|
||||
<ListRow key={document.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(document.id)}>
|
||||
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
|
||||
<FileText size={15} className="text-primary" />
|
||||
<span className="truncate">{document.title}</span>
|
||||
<SectionHeader title={t("ops.documentCenter")} count={totalCount} action={() => setEditingId(NEW_ENTITY_ID)} />
|
||||
{documents.length > 0 ? (
|
||||
<div>
|
||||
<div className="px-4 pt-3 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t("ops.documentsProse")}
|
||||
</div>
|
||||
<ListFrame>
|
||||
{documents.map((document) => (
|
||||
<ListRow key={document.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(document.id)}>
|
||||
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
|
||||
<FileText size={15} className="text-primary" />
|
||||
<span className="truncate">{document.title}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">{document.type.replaceAll("_", " ")} · v{document.version} · {document.updatedAt.slice(0, 10)}</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusPill status={document.status} />
|
||||
<button type="button" aria-label="Edit document" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" aria-label="Delete document" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">{document.type.replaceAll("_", " ")} · v{document.version} · {document.updatedAt.slice(0, 10)}</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusPill status={document.status} />
|
||||
<button type="button" aria-label="Edit document" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" aria-label="Delete document" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
</ListRow>
|
||||
))}
|
||||
</ListFrame>
|
||||
</div>
|
||||
) : null}
|
||||
{workProducts.length > 0 ? (
|
||||
<div>
|
||||
<div className="px-4 pt-4 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t("ops.documentsWorkProducts")}
|
||||
</div>
|
||||
<ListFrame>
|
||||
{workProducts.map((product) => (
|
||||
<WorkProductRow key={product.id} product={product} />
|
||||
))}
|
||||
</ListFrame>
|
||||
</div>
|
||||
) : null}
|
||||
<Modal open={editingId !== null} title={activeDocument ? activeDocument.title : "New document"} onClose={() => setEditingId(null)} width="760px">
|
||||
<DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === NEW_ENTITY_ID ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -871,6 +871,8 @@ export const en: Record<string, string> = {
|
||||
|
||||
// ── 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",
|
||||
|
||||
@@ -871,6 +871,8 @@ export const zh: Record<string, string> = {
|
||||
|
||||
// ── 公司运营 ──
|
||||
"ops.documentCenter": "文档中心",
|
||||
"ops.documentsProse": "文稿",
|
||||
"ops.documentsWorkProducts": "文件与交付物",
|
||||
"ops.knowledgeBase": "知识库",
|
||||
"ops.meetings": "会议",
|
||||
"ops.sprintBoard": "迭代看板",
|
||||
|
||||
@@ -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) {
|
||||
<DocumentCenter
|
||||
companyId={currentCompany.id}
|
||||
documents={companyDocuments}
|
||||
workProducts={companyWorkProducts}
|
||||
agents={companyAgents}
|
||||
projects={companyProjects}
|
||||
goals={companyGoals}
|
||||
|
||||
@@ -97,6 +97,26 @@ export interface DocumentRecord {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WorkProductRecord {
|
||||
id: string;
|
||||
companyId: string;
|
||||
projectId: string | null;
|
||||
taskId: string | null;
|
||||
type: string;
|
||||
provider: string;
|
||||
externalId: string | null;
|
||||
title: string;
|
||||
url: string | null;
|
||||
status: string;
|
||||
reviewState: string;
|
||||
isPrimary: boolean;
|
||||
summary: string | null;
|
||||
metadataJson: string | null;
|
||||
createdByRunId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeEntryRecord {
|
||||
id: string;
|
||||
companyId: string;
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
AutomationRuleRecord,
|
||||
AgentMessageRecord,
|
||||
WorkflowPipelineRecord,
|
||||
WorkProductRecord,
|
||||
} from "./records-extended";
|
||||
|
||||
export interface ProfileSnapshot {
|
||||
@@ -51,6 +52,7 @@ export interface ProfileSnapshot {
|
||||
socialAccounts: SocialAccountRecord[];
|
||||
meetings: MeetingRecord[];
|
||||
documents: DocumentRecord[];
|
||||
workProducts: WorkProductRecord[];
|
||||
knowledgeBase: KnowledgeEntryRecord[];
|
||||
sprints: SprintRecord[];
|
||||
automationRules: AutomationRuleRecord[];
|
||||
@@ -86,6 +88,7 @@ export const COMPANY_SCOPED_KEYS = [
|
||||
"socialAccounts",
|
||||
"meetings",
|
||||
"documents",
|
||||
"workProducts",
|
||||
"knowledgeBase",
|
||||
"sprints",
|
||||
"automationRules",
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("useAppStore", () => {
|
||||
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> = {}): ProfileSnapshot
|
||||
socialAccounts: [],
|
||||
meetings: [],
|
||||
documents: [],
|
||||
workProducts: [],
|
||||
knowledgeBase: [],
|
||||
sprints: [],
|
||||
automationRules: [],
|
||||
|
||||
Reference in new issue
Block a user