Skip to content

feat(desktop): copy a session from the session row menu - #3350

Open
czs914893385 wants to merge 1 commit into
GCWing:mainfrom
czs914893385:feat/desktop-session-duplicate
Open

czs914893385 wants to merge 1 commit into
GCWing:mainfrom
czs914893385:feat/desktop-session-duplicate

Conversation

@czs914893385

@czs914893385 czs914893385 commented Oct 11, 2026 •

Copy link
Copy Markdown

What

Adds Copy session to the session row menu (the row's "…" button and the right-click menu share one menu). The action produces a new independent session that carries the source session's persisted history; the source session is left untouched.

Why

The runtime already owns session copying, but only the CLI and the app-server exposed it:

  • AgentSessionForkPort::fork_session → fork_at_persisted_turn(.., ThroughTurn, ..), which copies every persisted turn, context snapshot, skill snapshot, compression transcript and evidence-ledger event under a new session id, clears inherited snapshot capability and resets child/relationship state.
  • CLI: "Fork from the full session" (ForkTarget::FullSession); app-server: session/fork.
  • The desktop session menu had no entry point for it, which is what users ask for as 会话复制 / "copy this session".

How

  • No new copy algorithm. The menu action calls the existing fork_session command, so there is still exactly one owner for session copying (the fork port).
  • ForkSessionRequest.source_turn_id becomes optional and wire-compatible (#[serde(default, skip_serializing_if = "Option::is_none")]). Omitting it means "copy the whole session": the host resolves the boundary from the session's own latest persisted turn. That keeps the controller from having to load a session's turns just to copy a navigation row. Existing callers that still send a turn id keep their current behaviour.
  • The WS/web adapter routes one command key by body: with a turn id → session/forkAtTurn, without → session/fork.
  • The copy is deliberately not added to the peer-device retry/idempotent allowlists: the host has no idempotency key for it, so a retry would create a second copy. Pinned by a test.
  • Naming reuses the existing fork lineage naming (Title → Title (2), collision-safe per workspace via resolve_branch_session_lineage), consistent with the resolution of [Bug]: fork分支的标题和原来的一致,应该加一个后缀做标识例如fork1等做区分 #1596. The title is persisted and shared by CLI/plugin/desktop forks, so it is intentionally not localized.
  • Availability: disabled while a turn is running (a streaming turn is not a stable copy boundary; matches the CLI's ActionAvailability::Idle gate for /fork) and for sessions owned by another device (no local history). Archived sessions can be copied. The copied session is projected from its authoritative metadata record into the nav list, and the active session is not switched — the same conservative behaviour as delete/archive.
  • i18n: en-US / zh-CN / zh-TW.

Testing

Rust (session copy chain):

cargo test -p openbitfun-core --no-default-features --features agent-runtime,git --lib branch_session
# ok. 8 passed; 0 failed

cargo test -p openbitfun-core --no-default-features --features agent-runtime,git --lib session_fork_
# ok. 4 passed; 0 failed

Frontend (touched suites):

pnpm --dir src/web-ui exec vitest run \
  src/flow_chat/services/flow-chat-manager/SessionModule.test.ts \
  src/infrastructure/api/adapters/websocket-adapter.test.ts \
  src/infrastructure/api/adapters/peer-device-adapter.test.ts \
  src/app/components/NavPanel/sections/sessions/sessionRowContextMenu.test.ts
# Test Files  4 passed (4)   Tests  159 passed (159)

Also run by the contributor: pnpm run check:web (canvas/appearance contracts + tsc) and node scripts/i18n-audit.mjs — both green.

The desktop crate compiles with this change (verified on Windows: cargo test -p openbitfun-desktop --lib builds the crate and its test harness successfully). The unit test added for the whole-session copy path is whole_session_copy_uses_the_latest_persisted_turn_fork.

Notes

  • No new dependencies; Cargo.lock and pnpm-lock.yaml are untouched.
  • The desktop fork_session command keeps its name and request shape on the wire; only the field becomes optional, so older frontends keep working unchanged.

中文说明

  • 在会话列表的行菜单(「⋯」按钮与右键菜单共用同一个菜单)里新增「复制会话」:把当前会话完整复制成一个新的独立会话,原会话保持不变。
  • 复制直接复用运行时已有的会话分叉能力(与 CLI 的 ForkTarget::FullSession、app-server 的 session/fork 是同一套实现),不新增第二套复制算法;ForkSessionRequest.source_turn_id 改为可选,省略时由宿主机按该会话最新已落盘的轮次复制,前端无需为一次菜单操作加载整个会话历史。
  • 正在对话中的会话(历史尚未稳定)与运行在其他设备上的会话不可复制,菜单项置灰并说明原因;归档会话可复制;复制后不自动切换到新会话。
  • 新会话标题沿用仓库既有的分叉命名规则(「标题 (2)」),与 [Bug]: fork分支的标题和原来的一致,应该加一个后缀做标识例如fork1等做区分 #1596 的处理一致。

Add "Copy session" to the session row menu (the row's more button and the
right-click menu share one menu). The copy is produced by the existing session
fork port instead of a second copy algorithm: ForkSessionRequest.source_turn_id
becomes optional, and omitting it copies the whole session through its latest
persisted turn, so the controller never has to load a session's turns for a
navigation action.

- route the web/WS adapter by body: session/forkAtTurn with a turn id,
  session/fork without one
- keep the copy out of the peer-device retry and idempotent allowlists: the
  host has no idempotency key for it, so a retry would create a second copy
- disable the action while a turn is running (not a stable copy boundary) and
  for sessions owned by another device; archived sessions can be copied and the
  active session is not switched
- reuse the existing fork lineage naming (title -> "title (2)")
- i18n: en-US / zh-CN / zh-TW
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.

2 participants