Skip to content

feat(groups): 群聊前端#761

Open
liqingb0220-stack wants to merge 4 commits into
dataelement:feature/unified-chat-single-agent-runtime-group-chatfrom
liqingb0220-stack:feat/group-chat-frontend
Open

feat(groups): 群聊前端#761
liqingb0220-stack wants to merge 4 commits into
dataelement:feature/unified-chat-single-agent-runtime-group-chatfrom
liqingb0220-stack:feat/group-chat-frontend

Conversation

@liqingb0220-stack

Copy link
Copy Markdown
Contributor

基于 feature/unified-chat-single-agent-runtime-group-chatc5db89a2)开发,只动前端,未改任何后端代码

新增 /groups 页面(侧边栏「群聊」入口),未改动 AgentDetailPage

内容

文件 作用
types/group.ts app/api/groups.py 的 schema 定义的类型
services/groupApi.ts /api/groups 全部接口的客户端
hooks/useGroupRealtime.ts 实时层:WebSocket 推送 / cursor 断线补拉 / 轮询兜底,三条路径对上层透明
pages/groups/ 群列表、会话列表、消息流、@ 补全、成员、群公告、群 workspace、群 memory
i18n/{zh,en}.json 中英文案

实现要点:

  • @ 提及走结构化 tokencontent 是纯文本,被 @ 的对象放在 mentions 数组单独传。只有从下拉框选中的才进数组——手打的 @名字 是纯文本,不会唤醒 Agent。与后端「只以结构化 mention token 为准」一致。
  • 发消息带前端生成的 message_id,重发不产生重复消息。
  • 群 workspace 复用现有 FileBrowser(通过 FileBrowserApi 适配器接入,未重写)。写入前先读一次拿 version_token 再提交——后端在 expected_version_token 为 null 时是无条件覆盖,不带 token 会让并发编辑互相冲掉。
  • 实时层是唯一需要跟后端一起改的地方。WS 端点和 after 游标就绪后,只改 useGroupRealtime.ts 一个文件,页面代码不动。

需要后端补的四个缺口

详见本 PR 带的 docs/group-chat/frontend-realtime-contract.md每一条都在真实后端上实测复现过(本地起了该分支的 postgres + redis + backend),不是推测:

# 缺口 实测结果
1 邀请成员拿不到 participant_id (participant_type, ref_id)422。而没有任何接口暴露 participant_id,Agent 的 participant 行还是懒创建的
2 群 workspace 不能上传文件 四个接口全是文本读写,无 multipart。PRD 2.10 要求「用户在群中上传的文件和资料」。前端已接 FileBrowser,上传能力只能先关掉
3 群 WebSocket 端点不存在 WS /ws/group/{id}403。前端已降级为轮询
4 消息列表没有 after 游标 after=被忽略,返回最新一页。前端改用从头部反向翻页补拉

1 是硬阻塞:不解决的话,群里除建群人外加不进任何人和 Agent,@ 唤醒和群 memory 都无法验证,群聊端到端跑不通

建议改法(改动面最小,前端已按此实现):让 POST /groups/{id}/members 额外接受 (participant_type, ref_id),服务端内部调 get_or_create_participant 解析(懒创建正好在这里发生)。这样前端用现成的 GET /agents/GET /org/users 就能构建候选列表,后端不需要新增查询接口。

顺带发现(与群聊无关,但影响整个分支)

  • GET /api/notifications/unread-count500relation "notifications" does not exist。分支上缺 notifications 表的迁移,全站通知栏都会报错。

一个 cursor 陷阱

cursor 形如 2026-07-14T06:16:04.577660+00:00|<uuid>,是微秒精度。JS 的 Date 只到毫秒,直接 Date.parse 比较会让同一毫秒内的两条消息判为相等、错序,进而在补拉时漏掉边界消息。本 PR 的 compareCursor 已处理(毫秒相等时回退到原始时间戳字符串比较)。如果后续要动 cursor 格式,请留意这一点。

验证

  • git am 到该分支基线:无冲突
  • tsc --noEmit:通过
  • npm run build:通过(GroupsPage 为独立 lazy chunk)
  • 浏览器实测:登录 → 建群 → 建会话 → 发消息 → 拉历史 → before 翻页 → @ 补全 → 群公告读写落盘 → 群 workspace 目录与文件读写,全部通过

🤖 Generated with Claude Code

Builds the /groups UI against the group API on
feature/unified-chat-single-agent-runtime-group-chat: group and session rails,
the message stream, @-mention autocomplete, members, announcement, group
workspace and per-agent group memory.

Mentions are sent as a structured `mentions` array, never parsed out of the
text, so only a name picked from the dropdown can wake an agent — matching the
backend contract. The group workspace reuses the existing FileBrowser through an
adapter that reads a file's version token before writing, so a concurrent edit is
rejected rather than silently overwritten.

Transport is isolated in useGroupRealtime: WebSocket push, cursor backfill on
reconnect, polling as a stopgap. Two of those cannot land yet — there is no
`WS /ws/group/{id}` and no `after` cursor on the message list — so the hook
settles into polling and backfills by paging backward. When the backend ships
them, only that file changes.

docs/group-chat/frontend-realtime-contract.md records the four backend gaps this
found, each reproduced against a live backend: invites reject (type, ref_id) and
nothing exposes participant_id (blocking), the group workspace cannot hold
uploaded files, no group WebSocket, and no `after` cursor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 936b2e86ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +32 to +33
const current = await groupApi.workspaceFile(groupId, path);
expected = current.exists ? current.version_token : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the loaded workspace token when saving

When two members edit the same workspace file, this reads the latest token immediately before saving, so a stale editor will overwrite any changes that landed after it loaded the file; the backend only rejects writes made after this fresh read, not edits made during the user's editing session. Store the version_token returned by read() and pass that original token into saveWorkspaceFile so intervening updates are rejected instead of silently clobbered.

Useful? React with 👍 / 👎.

Comment on lines +203 to +207
const intake = await groupApi.sendMessage(groupId, sessionId, {
content,
mentions: mentionParticipantIds.map((participant_id) => ({ participant_id })),
message_id: crypto.randomUUID(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the same message_id when retrying sends

If the POST reaches the backend but the client sees a network error, the composer keeps the draft so the user can send it again, but this line generates a new UUID for the retry. The backend idempotency check only deduplicates identical message_ids, so this failure mode creates duplicate group messages; generate and keep the id with the pending draft until it succeeds.

Useful? React with 👍 / 👎.

Comment on lines +83 to +84
// A mention the user deleted from the text should not wake its agent.
const live = picked.filter((member) => content.includes(`@${member.display_name}`));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track selected mention tokens instead of matching by name

Filtering live mentions with content.includes('@' + display_name) can send stale or unintended participants: for example, if @Ann was picked and later only @Ann Lee remains, @Ann Lee still contains @Ann, and duplicate display names have the same problem. In those cases the backend receives extra structured mention tokens and may wake the wrong agent, so this should track the inserted token/range or a less ambiguous marker rather than re-matching by display text.

Useful? React with 👍 / 👎.

Comment on lines +181 to +185
const startPolling = () => {
if (disposed || pollTimer) return;
setStatus('polling');
void catchUp();
pollTimer = setInterval(() => void catchUp(), POLL_INTERVAL_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh session summaries during polling fallback

With the current backend lacking /ws/group/{id}, the fallback polling path only calls catchUp(), which fetches messages for the active session. Messages arriving in other sessions never invoke onGroupActivity, so their unread badges and ordering stay stale until some unrelated refetch; the polling tick should also refresh the group session list or otherwise signal group activity.

Useful? React with 👍 / 👎.

Y1fe1Zh0u added a commit that referenced this pull request Jul 14, 2026
…ndaries

Group chat now uses REST as the authoritative write/history path, group-scoped WebSocket hints, and cursor backfill for recovery. The integration also aligns member identity contracts, workspace binary transport, transaction-bound storage compensation, and runtime tool transactions across the PR #761 frontend and unified runtime backend.

Constraint: WebSocket and Redis delivery are best-effort; REST-confirmed cursor and current membership remain authoritative
Constraint: Workspace uploads are capped at 50 MiB and group/path mutations require a Redis lock through commit or rollback
Rejected: Advancing recovery from WebSocket observations | out-of-order or dropped hints can skip messages
Rejected: Publishing before database commit | clients could observe rolled-back messages
Rejected: Permanent polling | it hides realtime failures and adds avoidable latency
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve callback-aware transaction boundaries, membership lock ordering, current-member delivery checks, and version-conditioned upload compensation
Tested: 135 focused backend tests, 122 expanded group/runtime tests, Ruff changed-file checks, compileall, alembic head/import checks, git diff --check, frontend TypeScript and Vite production build
Not-tested: Browser E2E and live 3010 acceptance are pending deployment; hard process crash between storage write and compensation remains a durable-outbox risk
Y1fe1Zh0u added a commit that referenced this pull request Jul 14, 2026
…ndaries

Group chat now uses REST as the authoritative write/history path, group-scoped WebSocket hints, and cursor backfill for recovery. The integration also aligns member identity contracts, workspace binary transport, transaction-bound storage compensation, and runtime tool transactions across the PR #761 frontend and unified runtime backend.

Constraint: WebSocket and Redis delivery are best-effort; REST-confirmed cursor and current membership remain authoritative
Constraint: Workspace uploads are capped at 50 MiB and group/path mutations require a Redis lock through commit or rollback
Rejected: Advancing recovery from WebSocket observations | out-of-order or dropped hints can skip messages
Rejected: Publishing before database commit | clients could observe rolled-back messages
Rejected: Permanent polling | it hides realtime failures and adds avoidable latency
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve callback-aware transaction boundaries, membership lock ordering, current-member delivery checks, and version-conditioned upload compensation
Tested: 135 focused backend tests, 122 expanded group/runtime tests, Ruff changed-file checks, compileall, alembic head/import checks, git diff --check, frontend TypeScript and Vite production build
Not-tested: Browser E2E and live 3010 acceptance are pending deployment; hard process crash between storage write and compensation remains a durable-outbox risk
Y1fe1Zh0u added a commit that referenced this pull request Jul 14, 2026
…ndaries

Group chat now uses REST as the authoritative write/history path, group-scoped WebSocket hints, and cursor backfill for recovery. The integration also aligns member identity contracts, workspace binary transport, transaction-bound storage compensation, and runtime tool transactions across the PR #761 frontend and unified runtime backend.

Constraint: WebSocket and Redis delivery are best-effort; REST-confirmed cursor and current membership remain authoritative
Constraint: Workspace uploads are capped at 50 MiB and group/path mutations require a Redis lock through commit or rollback
Rejected: Advancing recovery from WebSocket observations | out-of-order or dropped hints can skip messages
Rejected: Publishing before database commit | clients could observe rolled-back messages
Rejected: Permanent polling | it hides realtime failures and adds avoidable latency
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve callback-aware transaction boundaries, membership lock ordering, current-member delivery checks, and version-conditioned upload compensation
Tested: 135 focused backend tests, 122 expanded group/runtime tests, Ruff changed-file checks, compileall, alembic head/import checks, git diff --check, frontend TypeScript and Vite production build
Not-tested: Browser E2E and live 3010 acceptance are pending deployment; hard process crash between storage write and compensation remains a durable-outbox risk
Follow-up UI work on the group chat frontend:

- Nav: merge the group and session rails into one tree — groups are
  expandable nodes with their sessions nested; the group row shows
  name + unread roll-up + last-activity time (aggregated client-side
  since the list endpoint carries neither); active group expanded by
  default; empty groups show a "new session" guidance row.
- Header: breadcrumb — group name (small, clickable, ⋯ menu) over the
  session title. Side panel gains a group-name + gear top bar so it
  reads as group-scoped.
- Group settings modal (entries: tree ⋯, panel gear, breadcrumb ⋯):
  rename group, edit description, member management, dissolve. Panel is
  now view-and-invite only. Capabilities are shown by role, never
  surfaced-then-denied (rename/description/announcement/invite/create
  session for all human members; remove-member/dissolve/delete-session
  for managers) — matches the backend manager_only flags. No ownership
  transfer.
- Delete group + change the session delete affordance to a trash icon.
- Group memory tab reuses the FileBrowser work-list, matching the
  single-agent Mind tab.
- Composer: adopt the single-agent .chat-composer shell and fix the IME
  bug where Enter mid-composition sent raw pinyin (guard on
  isComposing / keyCode 229).
- Fix: cancelling the remove-member confirm no longer closes the
  settings modal (backdrop only closes on a direct click).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c5f498bbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const commit = async () => {
setBusy(true);
try {
await save(draft, data?.version_token ?? null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Save with the token from the loaded draft

When a member starts editing an announcement or memory file and this query refetches in the background after another member saves, data?.version_token is updated even though the dirty draft is intentionally preserved. Saving then sends the newer token with the stale draft, so the backend accepts the write and clobbers the intervening edit; keep the version token captured with the draft that is being edited instead of reading it from live query data at commit time.

Useful? React with 👍 / 👎.

Comment on lines +457 to +459
onClick={() => {
if (!isActiveGroup) navigate(`/groups/${group.id}`);
setShowSettings(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Open settings for the selected group only

When viewing group A and clicking the settings button on group B in the tree, this opens the modal before navigation has switched activeGroup; the modal can mount with A, and after the route changes to B its useState(group.name)/description values are not reset. Pressing Save then PATCHes group B with group A's stale name/description, so either key/reset the modal on group.id or store the clicked group as the settings target.

Useful? React with 👍 / 👎.

Comment on lines +27 to +29
useEffect(() => {
if (!agentRefId && agents.length > 0) setAgentRefId(agents[0].participant_ref_id);
}, [agents, agentRefId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset the memory agent when the group changes

With the side panel left on the Memory tab, switching from one group to another keeps the previous group's agentRefId because this effect only initializes when it is empty. If the new group has different agents, the FileBrowser calls /groups/{newGroupId}/agents/{oldAgentId}/memory, producing a 404 or showing no memory until the user manually picks an agent; reset the selection whenever the current id is not in the new agents list.

Useful? React with 👍 / 👎.

liqingb0220-stack and others added 2 commits July 15, 2026 16:21
- Sidebar Groups nav entry shows a total-unread badge (useGroupUnread sums every
  group's session unread, sharing the groups page's query cache). Per-group and
  per-session unread counts already render in the tree.
- Rename a session inline via a pencil on the row: the title becomes an editable
  field in place (any member can rename; only managers delete). Creating a group
  or session stays a modal.
- Unread badges use text-primary/bg-primary like the app's existing unread badge,
  not accent-primary/accent-text (same-hue, renders the number invisible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a draggable left-edge handle to the member panel; width is clamped
  240-520px and persisted to localStorage across sessions.
- Replace the breadcrumb ⋯ menu and connection-status text with a header
  subtitle showing the group's agent/member counts.
- Swap the panel toggle to sidebar collapse/expand icons with a
  state-aware tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant