diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 0eb0f03c..c77b46be 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -694,6 +694,10 @@ export const zh: Record = { "Resets {date} (UTC)": "{date} 重置(UTC)", "Unsaved": "未保存", "Working language": "工作语言", + "Direct: only its members take part, and every message wakes the other members. As the board, you can read it even when you are not a member.": "私聊:只有成员参与,每条消息都会唤醒其他成员。作为董事会,即使你不是成员也能阅读。", + "Company, division, department, project, pod and ad-hoc are labels for finding the conversation; they behave the same. An agent replies when @mentioned or already in the thread; a message that names no one goes to the most senior agent member.": "公司、事业部、部门、项目、小组和临时只是便于查找的标签,行为相同。Agent 在被 @提及或已在该话题中时回复;没有指名任何人的消息交给级别最高的 Agent 成员。", + "Workspace: everyone in the company is a member, including agents hired later, and any agent can read it.": "工作区:公司里的每个人都是成员,包括之后招聘的 Agent,任何 Agent 都能读取。", + "Restricted: only the members you pick take part; agents outside it cannot read it. As the board, you can read it even when you are not a member.": "受限:只有你选择的成员参与;其他 Agent 无法读取。作为董事会,即使你不是成员也能阅读。", "Your progress could not be saved: {error}": "无法保存你的进度:{error}", "Your progress was restored. Keys are never saved before launch, so enter yours again.": "已恢复你的进度。启动前不会保存密钥,请重新输入。", "Runs on its own: approves its risky work and large hires, and writes deliverables without review.": "自主运行:自行批准高风险工作和大额招聘,交付物不经审核直接写入。", diff --git a/src/renderer/src/pages/ChatPage.tsx b/src/renderer/src/pages/ChatPage.tsx index 60ac71b1..e0bbd07a 100644 --- a/src/renderer/src/pages/ChatPage.tsx +++ b/src/renderer/src/pages/ChatPage.tsx @@ -1,9 +1,7 @@ import { useCallback, useMemo, useRef, useState, type FormEvent } from "react"; -import { Field } from "../shared/ui"; -import { Dialog } from "../shared/Dialog"; -import { t } from "../i18n/i18n"; import { ChatSidebar } from "./chat/ChatSidebar"; import { ChatConversation } from "./chat/ChatConversation"; +import { NewSpaceDialog, type SpaceType, type Visibility } from "./chat/NewSpaceDialog"; import type { Draft, SendState } from "./chat/ChatComposer"; import { buildConversations, @@ -14,8 +12,6 @@ import { import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee } from "./types"; -type SpaceType = "company" | "division" | "department" | "project" | "pod" | "ad_hoc" | "direct"; -type Visibility = "workspace" | "restricted"; function value(form: FormData, key: string): string { return String(form.get(key) ?? "").trim(); @@ -269,45 +265,15 @@ export function ChatPage({ snapshot, actions }: PageProps) { composerRef={composerRef} /> - setCreateOpen(false)} onSubmit={createSpace} - > - - - - - - - - - - - - - - - + /> ); } diff --git a/src/renderer/src/pages/chat/NewSpaceDialog.tsx b/src/renderer/src/pages/chat/NewSpaceDialog.tsx new file mode 100644 index 00000000..9c05a81b --- /dev/null +++ b/src/renderer/src/pages/chat/NewSpaceDialog.tsx @@ -0,0 +1,81 @@ +import { useState, type FormEvent } from "react"; +import type { AgentRecord, EmployeeRecord } from "../../../../shared/domain"; +import { Dialog } from "../../shared/Dialog"; +import { Field } from "../../shared/ui"; +import { t } from "../../i18n/i18n"; + +export type SpaceType = "company" | "division" | "department" | "project" | "pod" | "ad_hoc" | "direct"; +export type Visibility = "workspace" | "restricted"; + +const hint = { fontSize: 12, color: "var(--text-3)", margin: 0, lineHeight: 1.5 } as const; + +/** Each choice says what it changes: who is a member, who can read it, and which agents a message wakes. */ +export function NewSpaceDialog({ open, employee, employees, agents, onClose, onSubmit }: { + open: boolean; + employee: EmployeeRecord; + employees: EmployeeRecord[]; + agents: AgentRecord[]; + onClose: () => void; + onSubmit: (event: FormEvent) => void; +}) { + const [spaceType, setSpaceType] = useState("project"); + const [visibility, setVisibility] = useState("workspace"); + const direct = spaceType === "direct"; + // A workspace channel takes in everyone, so picking members would change nothing. + const everyone = !direct && visibility === "workspace"; + return ( + + + + + + +

+ {direct + ? t("Direct: only its members take part, and every message wakes the other members. As the board, you can read it even when you are not a member.") + : t("Company, division, department, project, pod and ad-hoc are labels for finding the conversation; they behave the same. An agent replies when @mentioned or already in the thread; a message that names no one goes to the most senior agent member.")} +

+ {direct ? ( + + ) : ( + <> + + + +

+ {everyone + ? t("Workspace: everyone in the company is a member, including agents hired later, and any agent can read it.") + : t("Restricted: only the members you pick take part; agents outside it cannot read it. As the board, you can read it even when you are not a member.")} +

+ + )} + {!everyone && ( + <> + + + + + + + + )} +
+ ); +} diff --git a/tests/e2e/desktop.spec.ts b/tests/e2e/desktop.spec.ts index 85578b87..6f609016 100644 --- a/tests/e2e/desktop.spec.ts +++ b/tests/e2e/desktop.spec.ts @@ -166,6 +166,18 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await page.getByRole("button", { name: "New space" }).click(); await page.getByLabel("Name").fill("release"); await page.getByLabel("Topic").fill("Release coordination"); + // Each choice explains what it changes; member pickers appear only where they matter. + const spaceNotes = page.locator(".modal").getByRole("note"); + await expect(spaceNotes.filter({ hasText: "Workspace: everyone in the company is a member" })).toBeVisible(); + await expect(page.locator(".modal select[name=memberAgentIds]")).toHaveCount(0); + await page.locator(".modal select[name=visibility]").selectOption("restricted"); + await expect(spaceNotes.filter({ hasText: "Restricted: only the members you pick take part" })).toBeVisible(); + await expect(page.locator(".modal select[name=memberAgentIds]")).toHaveCount(1); + await page.locator(".modal select[name=spaceType]").selectOption("direct"); + await expect(spaceNotes.filter({ hasText: "Direct: only its members take part" })).toBeVisible(); + await expect(page.locator(".modal select[name=visibility]")).toHaveCount(0); + await page.locator(".modal select[name=spaceType]").selectOption("project"); + await page.locator(".modal select[name=visibility]").selectOption("workspace"); await page.locator(".modal").getByRole("button", { name: "Create" }).click(); await expect(page.getByText("release").first()).toBeVisible(); await page.getByRole("textbox", { name: "Message" }).fill("/escalate @Board review release plan");