Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

### Fixed

- #420:切换员工时岗位卡仍可显示上一位(#413 stale-while-revalidate),但裁撤 / 编辑只在 `card.data.id === selectedId` 时挂上。避免弹窗显示 A 的名字却对 B 发请求。

- #413:切换员工时工作台不再先清空成骨架屏。岗位卡与对话线程保持上一位员工的内容直到新数据返回;空态节点在加载中不入 DOM,且 `.owb-turn-thread--empty[hidden]` 盖过 `display:flex`;组织树选中底不再 120ms 交叉淡入,避免两行同时发亮。切到尚无 session 的员工时,待空 session 确认后再清掉上一位的线程。

- #412:失败回合里错误块与「创建新回合重试」之间补回气泡内既有 10–16px 间距节奏(retry 行 `margin-top: 12px`);错误块多行行高与正文对齐,并使用对称内边距。纯视觉,不改重试行为。
Expand Down
15 changes: 9 additions & 6 deletions apps/desktop/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1560,8 +1560,11 @@ function AppInner({

const engineOk = health?.engine?.available === true;
/** The frozen org-tree.v1 carries ids/budgets only; display names and modes
* arrive via the selected position card (/positions/:id). */
const selectedPosition = card.data;
* arrive via the selected position card (`/positions/:id`). Stale-while-revalidate
* may keep the previous card on screen (#413) while `selectedId` is already the
* next employee. Mutating actions wait until the record matches, or the dismiss
* dialog names A and deletes B (#420). */
const actionPosition = card.data?.id === selectedId ? card.data : null;
const positions = useMemo<PositionMentionOption[]>(() => {
if (!snapshot) return [];
return flattenPositionIds(snapshot.tree).map((id) => ({ id, name: positionNames[id] ?? t("org.unknownPosition") }));
Expand Down Expand Up @@ -2148,7 +2151,7 @@ function AppInner({
setMemorySource(source.kind === "mem_drive" ? "drive" : "docs");
setActiveModule("docs");
}}
actions={selectedPosition && selectedId ? (
actions={actionPosition ? (
<>
{/* Editing the record is available on every position, the
company owner included: the owner is an employee with
Expand All @@ -2160,15 +2163,15 @@ function AppInner({
<button
type="button"
className="owb-edit"
onClick={() => setEditTargetId(selectedId)}
onClick={() => setEditTargetId(actionPosition.id)}
disabled={orgBusy}
title={t("profile.editTitle")}
>
<PencilLine aria-hidden="true" size={13} />
{t("profile.edit")}
</button>
{selectedId !== snapshot?.owner ? (
<DismissPositionDialog positionName={selectedPosition.name} descendantCount={selectedNode ? countDescendants(selectedNode) : 0} busy={orgBusy} onDismiss={() => dismissPosition(selectedId)} />
{actionPosition.id !== snapshot?.owner ? (
<DismissPositionDialog positionName={actionPosition.name} descendantCount={selectedNode ? countDescendants(selectedNode) : 0} busy={orgBusy} onDismiss={() => dismissPosition(actionPosition.id)} />
) : null}
</>
) : undefined}
Expand Down
71 changes: 71 additions & 0 deletions apps/desktop/renderer/test/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,77 @@ it("#413 keeps employee A's card and thread while B's position fetch is in fligh
expect(screen.queryByText("历史结果")).not.toBeInTheDocument();
});

it("#420 does not dismiss B under A's name while B's position fetch is in flight", async () => {
const tree = {
...snapshot,
positionCount: 2,
tree: [{
...snapshot.tree[0]!,
children: [{ id: "docs-writer", reportTo: "repo-owner", budget: snapshot.tree[0]!.budget, children: [] }],
}],
};
const employeeB = { ...activeSession, positionId: "docs-writer", sessionId: "22222222-2222-4222-8222-222222222222" };
let holdB = false;
let finishB!: (value: Awaited<ReturnType<OwbBridge["position"]>>) => void;
const orgApply = vi.fn().mockResolvedValue({ status: 200, body: { status: "applied" } });
openedBridge({
orgTree: vi.fn().mockResolvedValue({ status: 200, body: tree }),
orgApply,
position: vi.fn((id: string) => {
if (id === "docs-writer" && holdB) {
return new Promise<Awaited<ReturnType<OwbBridge["position"]>>>((resolve) => { finishB = resolve; });
}
return Promise.resolve({ status: 200, body: { position: { ...position, id, name: id === "repo-owner" ? "代码库负责人" : id }, agentEngine: "qoder" } });
}),
sessions: vi.fn(async (id: string) => ({
status: 200,
body: {
schemaVersion: "workbench-session-list.v1",
positionId: id,
activeSessionId: id === "docs-writer" ? employeeB.sessionId : activeSession.sessionId,
sessions: id === "docs-writer" ? [employeeB] : [activeSession],
},
})),
sessionTurnHistory: vi.fn(async (sessionId: string) => ({
status: 200,
body: history([apiTurn({
conversationId: sessionId,
positionId: sessionId === employeeB.sessionId ? "docs-writer" : "repo-owner",
output: sessionId === employeeB.sessionId ? "B 的结果" : "历史结果",
})]),
})),
});
render(<App />);
const choose = async (id: string) => {
fireEvent.click((await screen.findByRole("tree")).querySelector(`[data-org-node-id="${id}"]`)!);
};
const card = () => screen.getByRole("region", { name: "岗位档案" });
await choose("repo-owner");
expect(await screen.findByRole("heading", { name: "代码库负责人" })).toBeInTheDocument();
expect(within(card()).getByRole("button", { name: "编辑" })).toBeInTheDocument();
expect(within(card()).queryByRole("button", { name: "裁撤" })).toBeNull();

holdB = true;
await choose("docs-writer");
expect(screen.getByRole("heading", { name: "代码库负责人" })).toBeInTheDocument();
expect(within(card()).queryByRole("button", { name: "裁撤" })).toBeNull();
expect(within(card()).queryByRole("button", { name: "编辑" })).toBeNull();
expect(screen.queryByRole("heading", { name: "确认裁撤 代码库负责人" })).toBeNull();

await act(async () => finishB({
status: 200,
body: { position: { ...position, id: "docs-writer", name: "文档负责人" }, agentEngine: "qoder" },
}));
expect(await within(card()).findByRole("heading", { name: "文档负责人" })).toBeInTheDocument();
fireEvent.click(within(card()).getByRole("button", { name: "裁撤" }));
expect(screen.getByRole("heading", { name: "确认裁撤 文档负责人" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "确认裁撤并留痕" }));
await waitFor(() => expect(orgApply).toHaveBeenCalledWith({
schemaVersion: "change-manifest.v1",
changes: [{ op: "delete", id: "docs-writer" }],
}));
});

it("#413 clears employee A's thread once B is confirmed to have no session", async () => {
const tree = {
...snapshot,
Expand Down
Loading