Q177: conversation types and visibility say what they change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd79f059ca
commit
f79e3d4d7c
4 files changed
+104
-41
No files matched your search
@@ -694,6 +694,10 @@ export const zh: Record<string, string> = {
|
||||
"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.": "自主运行:自行批准高风险工作和大额招聘,交付物不经审核直接写入。",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
title={t("New space")}
|
||||
<NewSpaceDialog
|
||||
key={createOpen ? "open" : "closed"}
|
||||
open={createOpen}
|
||||
submitLabel={t("Create")}
|
||||
employee={employee}
|
||||
employees={employees}
|
||||
agents={agents}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
onSubmit={createSpace}
|
||||
>
|
||||
<Field label={t("Name")}><input className="input" name="name" required /></Field>
|
||||
<Field label={t("Topic")}><input className="input" name="topic" required /></Field>
|
||||
<Field label={t("Type")}>
|
||||
<select className="input" name="spaceType" defaultValue="project">
|
||||
<option value="company">{t("Company")}</option>
|
||||
<option value="division">{t("Division")}</option>
|
||||
<option value="department">{t("Department")}</option>
|
||||
<option value="project">{t("Project")}</option>
|
||||
<option value="pod">{t("Pod")}</option>
|
||||
<option value="ad_hoc">{t("Ad-hoc")}</option>
|
||||
<option value="direct">{t("Direct")}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("Visibility")}>
|
||||
<select className="input" name="visibility" defaultValue="workspace">
|
||||
<option value="workspace">{t("Workspace")}</option>
|
||||
<option value="restricted">{t("Restricted")}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("Employees")}>
|
||||
<select className="input" name="memberEmployeeIds" multiple size={Math.max(1, Math.min(4, employees.length))}>
|
||||
{employees.filter((item) => item.id !== employee.id).map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.displayName}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("Agents")}>
|
||||
<select className="input" name="memberAgentIds" multiple size={Math.max(1, Math.min(4, agents.length))}>
|
||||
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
</Dialog>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLFormElement>) => void;
|
||||
}) {
|
||||
const [spaceType, setSpaceType] = useState<SpaceType>("project");
|
||||
const [visibility, setVisibility] = useState<Visibility>("workspace");
|
||||
const direct = spaceType === "direct";
|
||||
// A workspace channel takes in everyone, so picking members would change nothing.
|
||||
const everyone = !direct && visibility === "workspace";
|
||||
return (
|
||||
<Dialog title={t("New space")} open={open} submitLabel={t("Create")} onClose={onClose} onSubmit={onSubmit}>
|
||||
<Field label={t("Name")}><input className="input" name="name" required /></Field>
|
||||
<Field label={t("Topic")}><input className="input" name="topic" required /></Field>
|
||||
<Field label={t("Type")}>
|
||||
<select className="input" name="spaceType" value={spaceType} onChange={(event) => setSpaceType(event.target.value as SpaceType)}>
|
||||
<option value="company">{t("Company")}</option>
|
||||
<option value="division">{t("Division")}</option>
|
||||
<option value="department">{t("Department")}</option>
|
||||
<option value="project">{t("Project")}</option>
|
||||
<option value="pod">{t("Pod")}</option>
|
||||
<option value="ad_hoc">{t("Ad-hoc")}</option>
|
||||
<option value="direct">{t("Direct")}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<p style={hint} role="note">
|
||||
{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.")}
|
||||
</p>
|
||||
{direct ? (
|
||||
<input type="hidden" name="visibility" value="restricted" />
|
||||
) : (
|
||||
<>
|
||||
<Field label={t("Visibility")}>
|
||||
<select className="input" name="visibility" value={visibility} onChange={(event) => setVisibility(event.target.value as Visibility)}>
|
||||
<option value="workspace">{t("Workspace")}</option>
|
||||
<option value="restricted">{t("Restricted")}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<p style={hint} role="note">
|
||||
{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.")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{!everyone && (
|
||||
<>
|
||||
<Field label={t("Employees")}>
|
||||
<select className="input" name="memberEmployeeIds" multiple size={Math.max(1, Math.min(4, employees.length))}>
|
||||
{employees.filter((item) => item.id !== employee.id).map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.displayName}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("Agents")}>
|
||||
<select className="input" name="memberAgentIds" multiple size={Math.max(1, Math.min(4, agents.length))}>
|
||||
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user