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 ? (
+
- {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/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"
- />
- void addComment()}
- data-testid="org-task-comment-add"
- >
- {labels.save}
-
-
-
-
-
-
{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();
+ }}
+ />
+ }
+ onClick={() => void addComment()}
+ disabled={!newComment.trim()}
+ data-testid="org-task-comment-add"
+ >
+ {labels.send}
+
+
+
);
diff --git a/dashboard/src/org-ui/pages/tasks/TasksPage.module.css b/dashboard/src/org-ui/pages/tasks/TasksPage.module.css
index c1b305d1..d119d53b 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,19 +124,123 @@
.empty {
text-align: center;
- padding: 48px 12px;
+ padding: 64px 12px;
color: var(--fn-text-secondary, #667085);
}
-.detailGrid {
+.emptyIcon {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 12px;
+ opacity: 0.35;
+}
+
+.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;
}
}
@@ -133,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;
@@ -165,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/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]) => (
+ setFilterStatus(value)}
+ >
+ {label} ({count})
+
+ ))}
+
{loading ? (
{labels.loading}
) : visible.length === 0 ? (
+
+
+
{items.length === 0 ? labels.empty : labels.noMatch}
{labels.emptyHint}
) : (
{visible.map((task) => (
- onOpenTask?.(task.id)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onOpenTask?.(task.id);
+ }
+ }}
data-testid={`org-task-row-${task.id}`}
>
{statusLabel(task.status)}
@@ -282,7 +300,7 @@ export function TasksPage({
{nextLabel(task.status)}
) : null}
-
+
))}
)}
diff --git a/dashboard/src/org-ui/pages/tasks/labels.ts b/dashboard/src/org-ui/pages/tasks/labels.ts
index 3d7d8038..72e0f175 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:
@@ -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",
@@ -58,7 +61,7 @@ const EN = {
};
const ZH: typeof EN = {
- title: "任务",
+ title: "任务管理",
subtitle:
"宿主内的组织待办。存在 FREEOS_HOME/org,不是 Octop 定时任务,也不是项目对话或边车。",
hostHint:
@@ -106,6 +109,9 @@ const ZH: typeof EN = {
comments: "评论",
addComment: "添加评论…",
commentHint: "这是工作项上的人工备注,不是智能体对话。",
+ conversationEmpty: "还没有讨论。在下方发送第一条消息。",
+ send: "发送",
+ aiComment: "智能体",
createdBy: "创建者",
createdAt: "创建时间",
unknown: "未知",
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() {