From da15fa7fb2efdffc42571fc07f878a3ea2448bb1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 08:44:27 +0000 Subject: [PATCH 1/3] fix(org-ui): full-width Organization pages and scrollable bodies Remove the centered max-width column on migrated org-ui pages so they match sibling FreeOS Dashboard content width. Pin Organization path tabs below the title and scroll the PageShell body when content is taller than the viewport. Align list/empty/filter chrome closer to the original openXYOS OpenApp pages without changing host APIs. Co-authored-by: XYAI Labs --- dashboard/src/layouts/PageShell.module.less | 15 ++++ dashboard/src/layouts/PageShell.test.tsx | 11 +++ dashboard/src/layouts/PageShell.tsx | 13 ++- .../src/org-ui/layout/orgPage.module.css | 84 +++++++++++++++++++ .../org-ui/layout/orgPage.module.css.test.ts | 38 +++++++++ .../org-ui/pages/agents/AgentsPage.module.css | 8 +- .../announcements/AnnouncementPage.module.css | 46 ++++++++-- .../pages/announcements/AnnouncementPage.tsx | 23 ++--- .../pages/employees/EmployeesPage.module.css | 26 ++++-- .../pages/employees/EmployeesPage.test.tsx | 2 +- .../org-ui/pages/employees/EmployeesPage.tsx | 3 + .../src/org-ui/pages/employees/labels.ts | 8 +- .../governance/GovernancePage.module.css | 8 +- .../pages/governance/GovernancePage.test.tsx | 2 +- .../src/org-ui/pages/governance/labels.ts | 4 +- .../pages/knowledge/KnowledgePage.module.css | 10 +-- .../org-ui/pages/org/OrgChartPage.module.css | 9 +- .../reflections/ReflectionsPage.module.css | 8 +- .../reflections/ReflectionsPage.test.tsx | 2 +- .../src/org-ui/pages/reflections/labels.ts | 2 +- .../pages/settings/SettingsPage.module.css | 8 +- .../org-ui/pages/skills/SkillsPage.module.css | 11 +-- .../org-ui/pages/tasks/TasksPage.module.css | 35 ++++++-- .../src/org-ui/pages/tasks/TasksPage.test.tsx | 2 +- .../src/org-ui/pages/tasks/TasksPage.tsx | 50 +++++++---- dashboard/src/org-ui/pages/tasks/labels.ts | 4 +- .../pages/workspace/WorkspacePage.module.css | 8 +- .../src/pages/Organization/agents/index.tsx | 4 +- .../Organization/announcements/index.tsx | 4 +- .../pages/Organization/employees/detail.tsx | 4 +- .../pages/Organization/employees/index.tsx | 4 +- .../pages/Organization/governance/index.tsx | 4 +- .../src/pages/Organization/index.test.tsx | 2 + dashboard/src/pages/Organization/index.tsx | 8 +- .../pages/Organization/knowledge/index.tsx | 4 +- .../src/pages/Organization/org/index.tsx | 4 +- .../src/pages/Organization/orgPathTabs.tsx | 6 ++ .../pages/Organization/reflections/index.tsx | 4 +- .../src/pages/Organization/settings/index.tsx | 4 +- .../src/pages/Organization/skills/index.tsx | 4 +- .../src/pages/Organization/tasks/detail.tsx | 4 +- .../src/pages/Organization/tasks/index.tsx | 4 +- .../pages/Organization/workspace/index.tsx | 4 +- scripts/org-export/template/src/styles.css | 5 ++ 44 files changed, 368 insertions(+), 145 deletions(-) create mode 100644 dashboard/src/org-ui/layout/orgPage.module.css create mode 100644 dashboard/src/org-ui/layout/orgPage.module.css.test.ts diff --git a/dashboard/src/layouts/PageShell.module.less b/dashboard/src/layouts/PageShell.module.less index fbfdda0b..85988421 100644 --- a/dashboard/src/layouts/PageShell.module.less +++ b/dashboard/src/layouts/PageShell.module.less @@ -159,6 +159,21 @@ margin: -4px 0 12px; } +/** + * When `fill` or mobile path-tabs pin the chrome, scroll the remaining + * body so tall pages are not clipped by overflow:hidden on the card. + */ +.fillBody { + flex: 1; + min-height: 0; + min-width: 0; + width: 100%; + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; +} + /** * Pin Ant Design Tabs nav; only the active pane scrolls. * Use inside PageShell with `fill` (desktop). diff --git a/dashboard/src/layouts/PageShell.test.tsx b/dashboard/src/layouts/PageShell.test.tsx index a73e4838..c7ddca10 100644 --- a/dashboard/src/layouts/PageShell.test.tsx +++ b/dashboard/src/layouts/PageShell.test.tsx @@ -88,4 +88,15 @@ describe("PageShell path tabs", () => { container.querySelector("div[class*='pathTabsBelowTitle']"), ).toBeTruthy(); }); + + it("scrolls the body when fill pins the title chrome", () => { + render( + +
body
+
, + ); + const body = screen.getByTestId("page-shell-scroll-body"); + expect(body).toContainElement(screen.getByTestId("tall-body")); + expect(body.className).toMatch(/fillBody/); + }); }); diff --git a/dashboard/src/layouts/PageShell.tsx b/dashboard/src/layouts/PageShell.tsx index cbb915b4..4dca1e24 100644 --- a/dashboard/src/layouts/PageShell.tsx +++ b/dashboard/src/layouts/PageShell.tsx @@ -42,7 +42,10 @@ interface PageShellProps { pathTabsPlacement?: "title-row" | "below-title"; /** Render agent picker below the title row, outside the scrollable content card. */ agentScoped?: boolean; - /** When true, the content area does not scroll; children fill remaining height. */ + /** + * Pin title / path-tab chrome and let the remaining body scroll. + * Children that manage their own height (FillTabs) still fill this region. + */ fill?: boolean; children: React.ReactNode; } @@ -241,7 +244,13 @@ function PageShell({ )} - {children} + {pinBody ? ( +
+ {children} +
+ ) : ( + children + )} ); diff --git a/dashboard/src/org-ui/layout/orgPage.module.css b/dashboard/src/org-ui/layout/orgPage.module.css new file mode 100644 index 00000000..e6cf684f --- /dev/null +++ b/dashboard/src/org-ui/layout/orgPage.module.css @@ -0,0 +1,84 @@ +/** + * Shared Organization page body. Hosted under FreeOS PageShell and in the + * export-standalone shell — stretch to the content column instead of a + * centered card (openXYOS OpenApp used max-w-* inside a full-viewport app; + * Dashboard already insets by sidebar + PageShell padding). + */ +.page { + box-sizing: border-box; + width: 100%; + max-width: none; + min-width: 0; + margin: 0; + padding: 0 0 8px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.titleRow { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.title { + margin: 0; + font-size: 20px; + font-weight: 650; + line-height: 1.3; +} + +.subtitle { + margin: 4px 0 0; + color: var(--fn-text-secondary, #667085); + font-size: 13px; + line-height: 1.5; +} + +.empty { + text-align: center; + padding: 64px 16px; + color: var(--fn-text-secondary, #667085); +} + +.emptyIcon { + display: flex; + justify-content: center; + margin-bottom: 12px; + opacity: 0.35; +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; +} + +.stat { + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 10px; + padding: 12px 14px; + background: var(--fn-bg-container, #fff); +} + +.statValue { + font-size: 22px; + font-weight: 650; + line-height: 1.2; +} + +.statLabel { + margin-top: 4px; + font-size: 12px; + color: var(--fn-text-secondary, #667085); +} diff --git a/dashboard/src/org-ui/layout/orgPage.module.css.test.ts b/dashboard/src/org-ui/layout/orgPage.module.css.test.ts new file mode 100644 index 00000000..ec9b70f9 --- /dev/null +++ b/dashboard/src/org-ui/layout/orgPage.module.css.test.ts @@ -0,0 +1,38 @@ +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const pagesRoot = path.join(here, "..", "pages"); + +function walkCss(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return walkCss(full); + return entry.name.endsWith(".module.css") ? [full] : []; + }); +} + +describe("org-ui page layout", () => { + it("keeps the shared page shell full-bleed", () => { + const css = readFileSync(path.join(here, "orgPage.module.css"), "utf8"); + expect(css).toMatch(/width:\s*100%/); + expect(css).toMatch(/max-width:\s*none/); + expect(css).not.toMatch(/max-width:\s*\d+px/); + expect(css).not.toMatch(/margin:\s*0\s+auto/); + }); + + it("does not re-impose a centered max-width column on page wrappers", () => { + const files = walkCss(pagesRoot); + expect(files.length).toBeGreaterThan(5); + for (const file of files) { + const css = readFileSync(file, "utf8"); + const pageBlock = css.match(/\.page\s*\{[^}]*\}/); + expect(pageBlock, file).toBeTruthy(); + const block = pageBlock?.[0] ?? ""; + expect(block, file).not.toMatch(/max-width:\s*\d+px/); + expect(block, file).not.toMatch(/margin:\s*0\s+auto/); + } + }); +}); diff --git a/dashboard/src/org-ui/pages/agents/AgentsPage.module.css b/dashboard/src/org-ui/pages/agents/AgentsPage.module.css index 6cc7b921..3d4d581c 100644 --- a/dashboard/src/org-ui/pages/agents/AgentsPage.module.css +++ b/dashboard/src/org-ui/pages/agents/AgentsPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 680px; line-height: 1.5; } diff --git a/dashboard/src/org-ui/pages/announcements/AnnouncementPage.module.css b/dashboard/src/org-ui/pages/announcements/AnnouncementPage.module.css index 2ab9eb3c..46068281 100644 --- a/dashboard/src/org-ui/pages/announcements/AnnouncementPage.module.css +++ b/dashboard/src/org-ui/pages/announcements/AnnouncementPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 880px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -36,7 +31,36 @@ } .card { + position: relative; cursor: pointer; + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 10px; + padding: 16px; + background: var(--fn-bg-container, #fff); + transition: + box-shadow 0.15s ease, + border-color 0.15s ease; +} + +.card:hover { + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); +} + +.cardPinned { + border-color: color-mix(in srgb, var(--fn-color-brand, #1677ff) 30%, #e5e7eb); + box-shadow: 0 0 0 1px + color-mix(in srgb, var(--fn-color-brand, #1677ff) 12%, transparent); +} + +.cardExpired { + opacity: 0.65; +} + +.pinBadge { + position: absolute; + top: -8px; + left: -6px; + color: var(--fn-color-brand, #1677ff); } .cardTitle { @@ -64,6 +88,7 @@ margin-top: 8px; display: flex; flex-wrap: wrap; + align-items: center; gap: 12px; font-size: 12px; color: var(--fn-text-secondary, #667085); @@ -71,10 +96,17 @@ .empty { text-align: center; - padding: 48px 12px; + padding: 64px 12px; color: var(--fn-text-secondary, #667085); } +.emptyIcon { + display: flex; + justify-content: center; + margin-bottom: 12px; + opacity: 0.35; +} + .detailBody { white-space: pre-wrap; line-height: 1.6; diff --git a/dashboard/src/org-ui/pages/announcements/AnnouncementPage.tsx b/dashboard/src/org-ui/pages/announcements/AnnouncementPage.tsx index fcc3ae69..ffbb42b9 100644 --- a/dashboard/src/org-ui/pages/announcements/AnnouncementPage.tsx +++ b/dashboard/src/org-ui/pages/announcements/AnnouncementPage.tsx @@ -10,7 +10,7 @@ import { Tag, Typography, } from "antd"; -import { Megaphone, Pin, Plus, Search } from "lucide-react"; +import { Eye, Megaphone, Pin, Plus, Search } from "lucide-react"; import type { Announcement, OrgAnnouncementsClient, @@ -199,7 +199,7 @@ export function AnnouncementPage({

{labels.title}

{unreadCount > 0 ? ( - + {unreadCount} {labels.unread} ) : null} @@ -260,6 +260,9 @@ export function AnnouncementPage({
{labels.loading}
) : items.length === 0 ? (
+
+ +
{labels.empty}
) : ( @@ -271,7 +274,9 @@ export function AnnouncementPage({ return (
void openDetail(row)} role="button" @@ -279,16 +284,11 @@ export function AnnouncementPage({ onKeyDown={(event) => { if (event.key === "Enter") void openDetail(row); }} - style={{ - border: "1px solid var(--fn-border, #e5e7eb)", - borderRadius: 10, - padding: 14, - background: "var(--fn-bg-container, #fff)", - opacity: expired ? 0.65 : 1, - }} > + {row.is_pinned === 1 ? ( + + ) : null}
- {row.is_pinned === 1 ? : null} {typeLabel(row.type, locale)} @@ -338,6 +338,7 @@ export function AnnouncementPage({ {row.creator_name || labels.system} {format(row.published_at)} + {row.read_percent ?? 0}% ({row.read_count ?? 0}/ {row.total_users ?? 1}) diff --git a/dashboard/src/org-ui/pages/employees/EmployeesPage.module.css b/dashboard/src/org-ui/pages/employees/EmployeesPage.module.css index 85ddda5a..44fe953f 100644 --- a/dashboard/src/org-ui/pages/employees/EmployeesPage.module.css +++ b/dashboard/src/org-ui/pages/employees/EmployeesPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -80,6 +75,16 @@ cursor: pointer; text-align: left; width: 100%; + transition: + box-shadow 0.15s ease, + border-color 0.15s ease, + transform 0.15s ease; +} + +.card:hover { + border-color: var(--fn-primary, #1677ff); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); } .cardHead { @@ -121,10 +126,17 @@ .empty { text-align: center; - padding: 48px 12px; + padding: 64px 12px; color: var(--fn-text-secondary, #667085); } +.emptyIcon { + display: flex; + justify-content: center; + margin-bottom: 12px; + opacity: 0.35; +} + .detailGrid { display: grid; grid-template-columns: 1fr; diff --git a/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx b/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx index 5d8a235c..e698f355 100644 --- a/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx +++ b/dashboard/src/org-ui/pages/employees/EmployeesPage.test.tsx @@ -46,7 +46,7 @@ describe("EmployeesPage", () => { />, ); expect(await screen.findByTestId("org-ui-employees")).toBeInTheDocument(); - expect(screen.getByText("Employees")).toBeInTheDocument(); + expect(screen.getByText("Human–AI resources")).toBeInTheDocument(); expect(screen.getByTestId("org-employees-empty")).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); expect(client.list).toHaveBeenCalled(); diff --git a/dashboard/src/org-ui/pages/employees/EmployeesPage.tsx b/dashboard/src/org-ui/pages/employees/EmployeesPage.tsx index 06130a8e..7b26735b 100644 --- a/dashboard/src/org-ui/pages/employees/EmployeesPage.tsx +++ b/dashboard/src/org-ui/pages/employees/EmployeesPage.tsx @@ -201,6 +201,9 @@ export function EmployeesPage({

{labels.loading}

) : visible.length === 0 ? (
+
+ +

{departments.length === 0 ? labels.emptyHint : labels.empty}

{query ?

{labels.noMatch}

: null}
diff --git a/dashboard/src/org-ui/pages/employees/labels.ts b/dashboard/src/org-ui/pages/employees/labels.ts index f5c69abb..a49613ca 100644 --- a/dashboard/src/org-ui/pages/employees/labels.ts +++ b/dashboard/src/org-ui/pages/employees/labels.ts @@ -1,8 +1,8 @@ import type { OrgLocale } from "../../shell"; const EN = { - title: "Employees", - subtitle: "People and AI colleagues on the host org directory.", + title: "Human–AI resources", + subtitle: "Human–AI collaboration · host directory · full lifecycle", search: "Search name, role, or skills...", loading: "Loading employees...", empty: "No employees yet. Add a person after a department exists.", @@ -41,8 +41,8 @@ const EN = { }; const ZH: typeof EN = { - title: "员工目录", - subtitle: "宿主内的人机员工名册。", + title: "人机资源", + subtitle: "人机共融 · 宿主名册 · 全生命周期", search: "搜索姓名、岗位或技能...", loading: "加载员工...", empty: "还没有员工。先有部门后再添加人员。", diff --git a/dashboard/src/org-ui/pages/governance/GovernancePage.module.css b/dashboard/src/org-ui/pages/governance/GovernancePage.module.css index 56fac9c9..31bddc4b 100644 --- a/dashboard/src/org-ui/pages/governance/GovernancePage.module.css +++ b/dashboard/src/org-ui/pages/governance/GovernancePage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 1040px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -72,6 +67,7 @@ } .tableWrap { + width: 100%; overflow-x: auto; border: 1px solid var(--fn-border, #e5e7eb); border-radius: 12px; diff --git a/dashboard/src/org-ui/pages/governance/GovernancePage.test.tsx b/dashboard/src/org-ui/pages/governance/GovernancePage.test.tsx index 989e9bfa..fa328ebf 100644 --- a/dashboard/src/org-ui/pages/governance/GovernancePage.test.tsx +++ b/dashboard/src/org-ui/pages/governance/GovernancePage.test.tsx @@ -68,7 +68,7 @@ describe("GovernancePage", () => { , ); expect(await screen.findByTestId("org-ui-governance")).toBeInTheDocument(); - expect(screen.getByText("Governance")).toBeInTheDocument(); + expect(screen.getByText("Governance Engine")).toBeInTheDocument(); expect(screen.getByText("delete_employee")).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); expect(client.pauses).toHaveBeenCalled(); diff --git a/dashboard/src/org-ui/pages/governance/labels.ts b/dashboard/src/org-ui/pages/governance/labels.ts index 1bc35d57..1d08a351 100644 --- a/dashboard/src/org-ui/pages/governance/labels.ts +++ b/dashboard/src/org-ui/pages/governance/labels.ts @@ -1,7 +1,7 @@ import type { OrgLocale } from "../../shell"; const EN = { - title: "Governance", + title: "Governance Engine", subtitle: "Pending human approvals and the local host audit. High-risk tools stay blocked until someone decides.", loading: "Loading governance…", @@ -51,7 +51,7 @@ const EN = { }; const ZH: typeof EN = { - title: "治理", + title: "治理引擎", subtitle: "待人工复核的暂停,以及宿主本地审计。高风险工具在有人拍板前保持拦截。", loading: "加载治理…", diff --git a/dashboard/src/org-ui/pages/knowledge/KnowledgePage.module.css b/dashboard/src/org-ui/pages/knowledge/KnowledgePage.module.css index 7c1f7e09..9fd7804f 100644 --- a/dashboard/src/org-ui/pages/knowledge/KnowledgePage.module.css +++ b/dashboard/src/org-ui/pages/knowledge/KnowledgePage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 1040px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 640px; line-height: 1.5; } @@ -95,6 +89,7 @@ .card:hover, .cardSelected { border-color: var(--fn-primary, #1677ff); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); } .cardTitle { @@ -124,6 +119,7 @@ } .tableWrap { + width: 100%; overflow-x: auto; border: 1px solid var(--fn-border, #e5e7eb); border-radius: 12px; diff --git a/dashboard/src/org-ui/pages/org/OrgChartPage.module.css b/dashboard/src/org-ui/pages/org/OrgChartPage.module.css index ac8a0219..18aec83d 100644 --- a/dashboard/src/org-ui/pages/org/OrgChartPage.module.css +++ b/dashboard/src/org-ui/pages/org/OrgChartPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -45,6 +40,7 @@ display: flex; flex-direction: column; gap: 12px; + width: 100%; } .node { @@ -52,6 +48,7 @@ border-radius: 10px; padding: 14px; background: var(--fn-bg-container, #fff); + width: 100%; } .nodeHead { diff --git a/dashboard/src/org-ui/pages/reflections/ReflectionsPage.module.css b/dashboard/src/org-ui/pages/reflections/ReflectionsPage.module.css index 7b15ef45..ffbd4728 100644 --- a/dashboard/src/org-ui/pages/reflections/ReflectionsPage.module.css +++ b/dashboard/src/org-ui/pages/reflections/ReflectionsPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 640px; line-height: 1.5; } diff --git a/dashboard/src/org-ui/pages/reflections/ReflectionsPage.test.tsx b/dashboard/src/org-ui/pages/reflections/ReflectionsPage.test.tsx index 70151a24..a9c21f2d 100644 --- a/dashboard/src/org-ui/pages/reflections/ReflectionsPage.test.tsx +++ b/dashboard/src/org-ui/pages/reflections/ReflectionsPage.test.tsx @@ -59,7 +59,7 @@ describe("ReflectionsPage", () => { const client = mockClient(); render(); expect(await screen.findByTestId("org-ui-reflections")).toBeInTheDocument(); - expect(screen.getByText("Reflections")).toBeInTheDocument(); + expect(screen.getByText("Reflection Engine")).toBeInTheDocument(); expect(screen.getByText("const dead zone")).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); expect(client.list).toHaveBeenCalled(); diff --git a/dashboard/src/org-ui/pages/reflections/labels.ts b/dashboard/src/org-ui/pages/reflections/labels.ts index 20783658..b16d3093 100644 --- a/dashboard/src/org-ui/pages/reflections/labels.ts +++ b/dashboard/src/org-ui/pages/reflections/labels.ts @@ -1,7 +1,7 @@ import type { OrgLocale } from "../../shell"; const EN = { - title: "Reflections", + title: "Reflection Engine", subtitle: "Capture lessons, failures, and reusable experience on the host. Stored under FREEOS_HOME/org — not a sidecar and not Chat.", hostHint: diff --git a/dashboard/src/org-ui/pages/settings/SettingsPage.module.css b/dashboard/src/org-ui/pages/settings/SettingsPage.module.css index 130fb7d3..0263f9f1 100644 --- a/dashboard/src/org-ui/pages/settings/SettingsPage.module.css +++ b/dashboard/src/org-ui/pages/settings/SettingsPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 880px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 640px; line-height: 1.5; } diff --git a/dashboard/src/org-ui/pages/skills/SkillsPage.module.css b/dashboard/src/org-ui/pages/skills/SkillsPage.module.css index fcaa70e1..5bbe04d2 100644 --- a/dashboard/src/org-ui/pages/skills/SkillsPage.module.css +++ b/dashboard/src/org-ui/pages/skills/SkillsPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 1040px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 640px; line-height: 1.5; } @@ -94,6 +88,8 @@ .card:hover { border-color: var(--fn-primary, #1677ff); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); } .cardTitle { @@ -123,6 +119,7 @@ } .tableWrap { + width: 100%; overflow-x: auto; border: 1px solid var(--fn-border, #e5e7eb); border-radius: 12px; diff --git a/dashboard/src/org-ui/pages/tasks/TasksPage.module.css b/dashboard/src/org-ui/pages/tasks/TasksPage.module.css index c1b305d1..0d65b717 100644 --- a/dashboard/src/org-ui/pages/tasks/TasksPage.module.css +++ b/dashboard/src/org-ui/pages/tasks/TasksPage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -65,6 +60,17 @@ gap: 12px; } +.statusPills { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.search { + flex: 1; + min-width: 200px; +} + .hint { margin: 0; color: var(--fn-text-secondary, #667085); @@ -89,6 +95,14 @@ cursor: pointer; text-align: left; width: 100%; + transition: + background 0.15s ease, + border-color 0.15s ease; +} + +.row:hover { + background: var(--fn-bg, #f9fafb); + border-color: var(--fn-border, #d0d5dd); } .rowBody { @@ -110,10 +124,17 @@ .empty { text-align: center; - padding: 48px 12px; + padding: 64px 12px; color: var(--fn-text-secondary, #667085); } +.emptyIcon { + display: flex; + justify-content: center; + margin-bottom: 12px; + opacity: 0.35; +} + .detailGrid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); diff --git a/dashboard/src/org-ui/pages/tasks/TasksPage.test.tsx b/dashboard/src/org-ui/pages/tasks/TasksPage.test.tsx index 33fb3586..c4b565c9 100644 --- a/dashboard/src/org-ui/pages/tasks/TasksPage.test.tsx +++ b/dashboard/src/org-ui/pages/tasks/TasksPage.test.tsx @@ -59,7 +59,7 @@ describe("TasksPage", () => { const client = mockClient(); render(); expect(await screen.findByTestId("org-ui-tasks")).toBeInTheDocument(); - expect(screen.getByText("Tasks")).toBeInTheDocument(); + expect(screen.getByText("Task management")).toBeInTheDocument(); expect(screen.getByText("Ship Tasks slice")).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); expect(client.list).toHaveBeenCalled(); diff --git a/dashboard/src/org-ui/pages/tasks/TasksPage.tsx b/dashboard/src/org-ui/pages/tasks/TasksPage.tsx index a5d9d520..1762ac2d 100644 --- a/dashboard/src/org-ui/pages/tasks/TasksPage.tsx +++ b/dashboard/src/org-ui/pages/tasks/TasksPage.tsx @@ -208,6 +208,7 @@ export function TasksPage({
} value={search} onChange={(event) => setSearch(event.target.value)} @@ -215,19 +216,6 @@ export function TasksPage({ allowClear data-testid="org-tasks-search" /> -
+
+ {( + [ + ["all", labels.all, stats.total], + ["todo", labels.todo, stats.todo], + ["in_progress", labels.inProgress, stats.in_progress], + ["review", labels.review, stats.review], + ["done", labels.done, stats.done], + ] as const + ).map(([value, label, count]) => ( + + ))} +
{loading ? (

{labels.loading}

) : visible.length === 0 ? (
+
+ +

{items.length === 0 ? labels.empty : labels.noMatch}

{labels.emptyHint}

) : (
{visible.map((task) => ( - ) : null} - +
))}
)} diff --git a/dashboard/src/org-ui/pages/tasks/labels.ts b/dashboard/src/org-ui/pages/tasks/labels.ts index 3d7d8038..3fa13b1f 100644 --- a/dashboard/src/org-ui/pages/tasks/labels.ts +++ b/dashboard/src/org-ui/pages/tasks/labels.ts @@ -1,7 +1,7 @@ import type { OrgLocale } from "../../shell"; const EN = { - title: "Tasks", + title: "Task management", subtitle: "Organization work items on the host. Stored under FREEOS_HOME/org — not Octop cron, not project chat, and not a sidecar.", hostHint: @@ -58,7 +58,7 @@ const EN = { }; const ZH: typeof EN = { - title: "任务", + title: "任务管理", subtitle: "宿主内的组织待办。存在 FREEOS_HOME/org,不是 Octop 定时任务,也不是项目对话或边车。", hostHint: diff --git a/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css b/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css index a016ccf2..0629017d 100644 --- a/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css +++ b/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css @@ -1,10 +1,5 @@ .page { - max-width: 960px; - margin: 0 auto; - padding: 8px 4px 32px; - display: flex; - flex-direction: column; - gap: 16px; + composes: page from "../../layout/orgPage.module.css"; } .header { @@ -31,7 +26,6 @@ margin: 4px 0 0; color: var(--fn-text-secondary, #667085); font-size: 13px; - max-width: 640px; line-height: 1.5; } diff --git a/dashboard/src/pages/Organization/agents/index.tsx b/dashboard/src/pages/Organization/agents/index.tsx index 468c5e74..327fc243 100644 --- a/dashboard/src/pages/Organization/agents/index.tsx +++ b/dashboard/src/pages/Organization/agents/index.tsx @@ -8,7 +8,7 @@ import { createOrgApiClient, type OrgSession, } from "../../../org-ui"; -import { useOrgPathTabs } from "../orgPathTabs"; +import { ORG_PAGE_SHELL, useOrgPathTabs } from "../orgPathTabs"; export default function OrganizationAgentsPage() { const { t, i18n } = useTranslation(); @@ -33,7 +33,7 @@ export default function OrganizationAgentsPage() { diff --git a/dashboard/src/pages/Organization/announcements/index.tsx b/dashboard/src/pages/Organization/announcements/index.tsx index 89bda143..36597550 100644 --- a/dashboard/src/pages/Organization/announcements/index.tsx +++ b/dashboard/src/pages/Organization/announcements/index.tsx @@ -10,7 +10,7 @@ import { type OrgSession, } from "../../../org-ui"; import { formatServerIsoDateTime } from "../../../utils/formatMessageTime"; -import { useOrgPathTabs } from "../orgPathTabs"; +import { ORG_PAGE_SHELL, useOrgPathTabs } from "../orgPathTabs"; export default function OrganizationAnnouncementsPage() { const { t, i18n } = useTranslation(); @@ -36,7 +36,7 @@ export default function OrganizationAnnouncementsPage() { { expect(screen.queryByTestId("org-restart-overlay")).toBeNull(); expect(orgModuleApi.startSidecar).not.toHaveBeenCalled(); expect(orgModuleApi.probeLivez).not.toHaveBeenCalled(); + expect(screen.getByTestId("page-shell-scroll-body")).toBeInTheDocument(); + expect(screen.getAllByTestId("path-tabs-row")).toHaveLength(2); }); it("does not auto-start or gate login when the optional sidecar is down", async () => { diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx index f6d0d7eb..c338685f 100644 --- a/dashboard/src/pages/Organization/index.tsx +++ b/dashboard/src/pages/Organization/index.tsx @@ -36,7 +36,7 @@ import { } from "../../utils/desktopFolder"; import { message } from "../../utils/antdMessage"; import { resolveOpenxyosSourceDest } from "./pickSourceDest"; -import { useOrgPathTabs } from "./orgPathTabs"; +import { ORG_PAGE_SHELL, useOrgPathTabs } from "./orgPathTabs"; import styles from "./Organization.module.less"; type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | "produce" | null; @@ -362,7 +362,11 @@ export default function OrganizationPage() { const pathTabs = useOrgPathTabs("workbench"); return ( - +
diff --git a/dashboard/src/pages/Organization/knowledge/index.tsx b/dashboard/src/pages/Organization/knowledge/index.tsx index bbeb0f24..5bbcb801 100644 --- a/dashboard/src/pages/Organization/knowledge/index.tsx +++ b/dashboard/src/pages/Organization/knowledge/index.tsx @@ -10,7 +10,7 @@ import { type OrgSession, } from "../../../org-ui"; import { userCan } from "../../../utils/permissions"; -import { useOrgPathTabs } from "../orgPathTabs"; +import { ORG_PAGE_SHELL, useOrgPathTabs } from "../orgPathTabs"; export default function OrganizationKnowledgePage() { const { t, i18n } = useTranslation(); @@ -36,7 +36,7 @@ export default function OrganizationKnowledgePage() { diff --git a/dashboard/src/pages/Organization/orgPathTabs.tsx b/dashboard/src/pages/Organization/orgPathTabs.tsx index 9ff4114e..a9f29165 100644 --- a/dashboard/src/pages/Organization/orgPathTabs.tsx +++ b/dashboard/src/pages/Organization/orgPathTabs.tsx @@ -16,6 +16,12 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import type { PathTabsConfig } from "../../layouts/PageShell"; +/** Shared PageShell chrome for /organization/* — full-bleed, scrollable body. */ +export const ORG_PAGE_SHELL = { + fill: true, + pathTabsPlacement: "below-title", +} as const; + export function useOrgPathTabs( active: | "workbench" diff --git a/dashboard/src/pages/Organization/reflections/index.tsx b/dashboard/src/pages/Organization/reflections/index.tsx index e9777ca1..2e00c19b 100644 --- a/dashboard/src/pages/Organization/reflections/index.tsx +++ b/dashboard/src/pages/Organization/reflections/index.tsx @@ -8,7 +8,7 @@ import { createOrgApiClient, type OrgSession, } from "../../../org-ui"; -import { useOrgPathTabs } from "../orgPathTabs"; +import { ORG_PAGE_SHELL, useOrgPathTabs } from "../orgPathTabs"; export default function OrganizationReflectionsPage() { const { t, i18n } = useTranslation(); @@ -33,7 +33,7 @@ export default function OrganizationReflectionsPage() { Date: Sat, 19 Sep 2026 08:44:59 +0000 Subject: [PATCH 2/3] docs: note Organization subpage layout fix in changelog Co-authored-by: XYAI Labs --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796e1162..1518077d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ ### 修复 +- Organization 子页(公告 / 架构 / 员工 / 技能 / 治理 / 知识 / 任务 / 反思 / 设置 / 智能体 / 工作台)去掉居中窄列 `max-width`,与知识库、设置等 Dashboard 系统页同宽;`PageShell` 在 `fill` 时滚动内容区,避免被固定标题/路径 Tab 裁切。路径 Tab 改到标题下方两行,页面主体更接近 openXYOS OpenApp(Ant Design 控件 vs 原 Tailwind 仍保留)。Chat 仍是 FreeOS 原生。 + > 下面若干「启动即拉起本机 openXYOS」条目记录的是 0.0.1 边车时代问题。默认路径已被 [0.0.2] 与本文件 Unreleased 的 Phase 5 取代:安装与首屏不再捆绑或自动拉起 Node。 - Windows 安装预配不再把 Node 工作目录选到残留的嵌套 `openxyos\\openxyos`:顶层已有 `backend-dist/server.js` 与 `dist/index.html` 时必须用 live 根。嵌套 cwd 会让 `node backend-dist/server.js` 立刻退出且 stdout/stderr 为空,安装空等 90s 后以退出码 12 失败。`start-sidecar.ps1` / 预配 / 桌面 Go / Python 启动路径统一按此选择 cwd;每次启动截断 `start.log`;Shell.Application 若未刷新日志则改走 explorer / Start-Process,Node fail-fast 不再伪装成 livez 超时。 From c275ba2ae4158f6c5595ab9b7d8657faf291104e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 08:54:35 +0000 Subject: [PATCH 3/3] fix(org-ui): merge task comments into an embedded conversation pane Task detail no longer splits a comment list from an empty right-hand detail column. Discussion lives in one chat-style panel (thread + composer) beside task metadata, still using host task comments. Co-authored-by: XYAI Labs --- CHANGELOG.md | 1 + .../pages/tasks/TaskDetailPage.test.tsx | 56 +++++ .../src/org-ui/pages/tasks/TaskDetailPage.tsx | 216 ++++++++++++++---- .../org-ui/pages/tasks/TasksPage.module.css | 164 ++++++++++++- dashboard/src/org-ui/pages/tasks/labels.ts | 6 + 5 files changed, 388 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1518077d..677dd8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### 修复 - Organization 子页(公告 / 架构 / 员工 / 技能 / 治理 / 知识 / 任务 / 反思 / 设置 / 智能体 / 工作台)去掉居中窄列 `max-width`,与知识库、设置等 Dashboard 系统页同宽;`PageShell` 在 `fill` 时滚动内容区,避免被固定标题/路径 Tab 裁切。路径 Tab 改到标题下方两行,页面主体更接近 openXYOS OpenApp(Ant Design 控件 vs 原 Tailwind 仍保留)。Chat 仍是 FreeOS 原生。 +- Organization 任务详情把原来的「评论列表 + 右侧空白详情」收成一块嵌入式讨论区:左侧任务信息 / 子任务 / 状态,右侧消息流 + 发送框(头像、气泡、智能体备注),仍走宿主 `/api/org-module/tasks/:id/comments`,不嵌入 FreeOS Chat。 > 下面若干「启动即拉起本机 openXYOS」条目记录的是 0.0.1 边车时代问题。默认路径已被 [0.0.2] 与本文件 Unreleased 的 Phase 5 取代:安装与首屏不再捆绑或自动拉起 Node。 diff --git a/dashboard/src/org-ui/pages/tasks/TaskDetailPage.test.tsx b/dashboard/src/org-ui/pages/tasks/TaskDetailPage.test.tsx index 8a3b5a75..1f159dcf 100644 --- a/dashboard/src/org-ui/pages/tasks/TaskDetailPage.test.tsx +++ b/dashboard/src/org-ui/pages/tasks/TaskDetailPage.test.tsx @@ -84,10 +84,66 @@ describe("TaskDetailPage", () => { "Human notes on this work item. This is not agent chat.", ), ).toBeInTheDocument(); + expect(screen.getByTestId("org-task-conversation")).toBeInTheDocument(); + expect( + screen.getByTestId("org-task-conversation-empty"), + ).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); expect(client.get).toHaveBeenCalledWith(3); }); + it("shows comments in one conversation pane instead of a split list", async () => { + const client = mockClient(); + client.get = vi.fn(async () => ({ + id: 3, + title: "Ship Tasks slice", + description: "Host org tasks", + status: "todo", + priority: "high", + creator_name: "Ada", + created_at: "2026-09-19T00:00:00Z", + subtasks: [], + comments: [ + { + id: 8, + task_id: 3, + content: "Looks good", + comment_type: "user", + user_name: "Ada", + created_at: "2026-09-19T00:00:00Z", + }, + { + id: 9, + task_id: 3, + content: "Starting now", + comment_type: "ai", + user_name: "Scout", + created_at: "2026-09-19T00:01:00Z", + }, + ], + })); + render( + , + ); + expect( + await screen.findByTestId("org-task-conversation"), + ).toBeInTheDocument(); + expect( + screen.getByTestId("org-task-conversation-thread"), + ).toBeInTheDocument(); + expect(screen.getByTestId("org-task-comment-8")).toBeInTheDocument(); + expect(screen.getByText("Looks good")).toBeInTheDocument(); + expect(screen.getByText("Starting now")).toBeInTheDocument(); + expect(screen.getByText("Scout")).toBeInTheDocument(); + expect(screen.queryByTestId("org-task-conversation-empty")).toBeNull(); + expect(document.querySelector("iframe")).toBeNull(); + }); + it("saves edits and adds a comment through the injected client", async () => { const client = mockClient(); render( diff --git a/dashboard/src/org-ui/pages/tasks/TaskDetailPage.tsx b/dashboard/src/org-ui/pages/tasks/TaskDetailPage.tsx index 323f8ca6..209080c7 100644 --- a/dashboard/src/org-ui/pages/tasks/TaskDetailPage.tsx +++ b/dashboard/src/org-ui/pages/tasks/TaskDetailPage.tsx @@ -1,7 +1,18 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Button, Input, Select, Space, Tag, Typography } from "antd"; -import { ArrowLeft, ListTodo } from "lucide-react"; -import type { OrgTask, OrgTasksClient } from "../../api/createClient"; +import { + ArrowLeft, + Bot, + CheckCircle, + ListTodo, + MessageSquare, + Send, +} from "lucide-react"; +import type { + OrgTask, + OrgTaskComment, + OrgTasksClient, +} from "../../api/createClient"; import type { OrgLocale, OrgSession } from "../../shell"; import { taskLabels } from "./labels"; import styles from "./TasksPage.module.css"; @@ -23,6 +34,37 @@ function nextStatus(status: string): string | null { return null; } +function statusColor(status: string): string { + return ( + { + todo: "blue", + in_progress: "gold", + review: "purple", + done: "green", + }[status] || "default" + ); +} + +function priorityColor(value: string): string { + return ( + { + critical: "red", + high: "orange", + medium: "blue", + low: "default", + }[value] || "default" + ); +} + +function formatStamp(value: string): string { + return (value || "").replace("T", " ").slice(0, 16); +} + +function commentInitial(comment: OrgTaskComment): string { + const name = (comment.user_name || "").trim(); + return name ? name.slice(0, 1).toUpperCase() : "U"; +} + export function TaskDetailPage({ client, session: _session, @@ -31,6 +73,7 @@ export function TaskDetailPage({ onBack, }: TaskDetailPageProps) { const labels = taskLabels(locale); + const threadRef = useRef(null); const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); @@ -83,6 +126,13 @@ export function TaskDetailPage({ void load(); }, [load]); + const comments = task?.comments || []; + useEffect(() => { + const node = threadRef.current; + if (!node) return; + node.scrollTop = node.scrollHeight; + }, [comments.length, loading]); + const save = async () => { if (!title.trim()) { setError(labels.required); @@ -181,9 +231,10 @@ export function TaskDetailPage({ const done = (task.subtasks || []).filter((row) => row.completed).length; const total = (task.subtasks || []).length; + const progress = total ? Math.round((done / total) * 100) : 0; return ( -
+
{onBack ? ( @@ -220,10 +271,9 @@ export function TaskDetailPage({
{error ? {error} : null} -

{labels.commentHint}

-
-
+
+
{editing ? ( <> @@ -266,8 +316,12 @@ export function TaskDetailPage({ ) : ( <> - {statusLabel(task.status)} - {priorityLabel(task.priority)} + + {statusLabel(task.status)} + + + {priorityLabel(task.priority)} + {task.title} @@ -283,15 +337,27 @@ export function TaskDetailPage({ {labels.createdAt} {(task.created_at || "").split("T")[0] || "—"}
+
+ {labels.assignee} + {task.assignee_name || labels.unassigned} +
)}
-
+
{labels.subtasks} {total ? ` (${done}/${total})` : ""} + {total ? ( +
+
+
+ ) : null} {(task.subtasks || []).map((sub) => (
-
- - {labels.comments} ({(task.comments || []).length}) - - {(task.comments || []).map((comment) => ( -
-
- {comment.user_name || labels.unknown} ·{" "} - {(comment.created_at || "").replace("T", " ").slice(0, 16)} -
-

{comment.content}

-
- ))} -
- setNewComment(event.target.value)} - placeholder={labels.addComment} - rows={2} - data-testid="org-task-comment-input" - /> - -
-
-
- -
{labels.status} {nextStatus(task.status) ? ( @@ -377,14 +410,101 @@ export function TaskDetailPage({ {nextLabel(task.status)} ) : ( -

{labels.completed}

+
+
+ +
+

{labels.completed}

+
)}
-
- {labels.assignee} -

{task.assignee_name || labels.unassigned}

-
+ +
+
+

+ + {labels.comments} ({comments.length}) +

+

{labels.commentHint}

+
+
+ {comments.length === 0 ? ( +
+
+ +
+

{labels.conversationEmpty}

+
+ ) : ( + comments.map((comment) => { + const isAi = comment.comment_type === "ai"; + return ( +
+
+ {isAi ? : commentInitial(comment)} +
+
+
+ + {comment.user_name || + (isAi ? labels.aiComment : labels.unknown)} + + {formatStamp(comment.created_at)} +
+

+ {comment.content} +

+
+
+ ); + }) + )} +
+
+ setNewComment(event.target.value)} + placeholder={labels.addComment} + rows={2} + data-testid="org-task-comment-input" + onPressEnter={(event) => { + if (event.shiftKey) return; + event.preventDefault(); + void addComment(); + }} + /> + +
+
); diff --git a/dashboard/src/org-ui/pages/tasks/TasksPage.module.css b/dashboard/src/org-ui/pages/tasks/TasksPage.module.css index 0d65b717..d119d53b 100644 --- a/dashboard/src/org-ui/pages/tasks/TasksPage.module.css +++ b/dashboard/src/org-ui/pages/tasks/TasksPage.module.css @@ -135,15 +135,112 @@ opacity: 0.35; } -.detailGrid { +.pageFill { + composes: page; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.workspace { display: grid; - grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); + grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); gap: 16px; + flex: 1; + min-height: 0; +} + +.stack { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; +} + +.conversation { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + overflow: hidden; + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 12px; + background: var(--fn-bg-container, #fff); +} + +.conversationHeader { + flex-shrink: 0; + padding: 14px 16px 12px; + border-bottom: 1px solid var(--fn-border, #f0f0f0); +} + +.conversationTitle { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 14px; + font-weight: 600; +} + +.conversationThread { + flex: 1; + min-height: 220px; + overflow-x: hidden; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.conversationEmpty { + margin: auto; + text-align: center; + color: var(--fn-text-secondary, #667085); + font-size: 13px; +} + +.conversationEmptyIcon { + display: flex; + justify-content: center; + margin-bottom: 10px; + opacity: 0.35; +} + +.conversationComposer { + flex-shrink: 0; + display: flex; + align-items: flex-end; + gap: 8px; + padding: 12px 16px 14px; + border-top: 1px solid var(--fn-border, #f0f0f0); +} + +.composerInput { + flex: 1; + min-width: 0; } @media (max-width: 720px) { - .detailGrid { + .pageFill { + overflow: visible; + } + + .workspace { grid-template-columns: 1fr; + overflow: visible; + } + + .stack { + overflow: visible; + } + + .conversation { + min-height: 360px; } } @@ -154,6 +251,20 @@ background: var(--fn-bg-container, #fff); } +.progress { + height: 6px; + border-radius: 99px; + background: var(--fn-fill-tertiary, #f2f4f7); + margin: 0 0 12px; + overflow: hidden; +} + +.progressBar { + height: 100%; + border-radius: 99px; + background: var(--fn-color-brand, #1677ff); +} + .rowLine { display: flex; justify-content: space-between; @@ -186,20 +297,59 @@ } .comment { - padding: 8px 0; - border-bottom: 1px solid var(--fn-border, #f0f0f0); + display: flex; + align-items: flex-start; + gap: 10px; } -.comment:last-child { - border-bottom: none; +.avatar { + flex-shrink: 0; + width: 32px; + height: 32px; + border-radius: 999px; + display: flex; + align-items: center; + justify-content: center; + background: var(--fn-fill-tertiary, #eef2ff); + color: var(--fn-color-brand, #1677ff); + font-size: 12px; + font-weight: 700; +} + +.commentBody { + flex: 1; + min-width: 0; } .commentMeta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; font-size: 12px; color: var(--fn-text-secondary, #667085); margin-bottom: 4px; } +.commentAuthor { + font-weight: 600; + color: var(--fn-text, #101828); +} + +.commentBubble { + margin: 0; + padding: 8px 10px; + border-radius: 10px; + background: var(--fn-bg, #f8fafc); + white-space: pre-wrap; + line-height: 1.6; + font-size: 14px; +} + +.commentBubbleAi { + background: var(--fn-fill-tertiary, #eef2ff); +} + .addRow { display: flex; gap: 8px; diff --git a/dashboard/src/org-ui/pages/tasks/labels.ts b/dashboard/src/org-ui/pages/tasks/labels.ts index 3fa13b1f..72e0f175 100644 --- a/dashboard/src/org-ui/pages/tasks/labels.ts +++ b/dashboard/src/org-ui/pages/tasks/labels.ts @@ -49,6 +49,9 @@ const EN = { comments: "Comments", addComment: "Add a comment…", commentHint: "Human notes on this work item. This is not agent chat.", + conversationEmpty: "No discussion yet. Send the first message below.", + send: "Send", + aiComment: "AI", createdBy: "Created by", createdAt: "Created", unknown: "Unknown", @@ -106,6 +109,9 @@ const ZH: typeof EN = { comments: "评论", addComment: "添加评论…", commentHint: "这是工作项上的人工备注,不是智能体对话。", + conversationEmpty: "还没有讨论。在下方发送第一条消息。", + send: "发送", + aiComment: "智能体", createdBy: "创建者", createdAt: "创建时间", unknown: "未知",