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
4 changes: 2 additions & 2 deletions docs/interactive-capabilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, a
- Generated per-item interaction audit: `docs/interactive-capabilities/technical/product-control-open-audit.json`
- Generated low-level audit map: `docs/interactive-capabilities/technical/tauri-command-map.json`

说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **673** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。
说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **674** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。

Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **673** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.
Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **674** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.

## 控制边界 / Control boundary

Expand Down
10 changes: 5 additions & 5 deletions docs/interactive-capabilities/capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"title": "OpenBitFun Playbook",
"origin": "https://playbook.openbitfun.com",
"source": "src/shared/interactive-capabilities/catalog.json",
"digest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
"digest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
"ownerDigest": "c989ace53614c2a526fefe64816fdd6895ec71edcc28f44b6f80d1edb75bb33e",
"searchAcceptance": [
{
Expand Down Expand Up @@ -19435,8 +19435,8 @@
},
{
"id": "session-lifecycle",
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
"control": {
"kind": "open",
"reasonCode": "visualSelection",
Expand Down Expand Up @@ -19735,8 +19735,8 @@
"Safely customize the running OpenBitFun frontend in Creative mode",
"打开并新建一个 AI 会话",
"Open and create a new AI session",
"重命名、归档、恢复、删除、分叉与导出会话",
"Rename, archive, restore, delete, fork, and export sessions",
"重命名、归档、恢复、删除、复制、分叉与导出会话",
"Rename, archive, restore, delete, copy, fork, and export sessions",
"为会话选择模型、工作模式、记忆模式与权限模式",
"Choose the model, work mode, memory mode, and permission mode per session",
"让创造模式扩展客户端界面与运行时能力",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Work with AI in project context, create sessions, continue tasks, answer permiss

- **智能体可直接控制 / Direct Agent control** · 打开并新建一个 AI 会话
- Open and create a new AI session
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 重命名、归档、恢复、删除、分叉与导出会话
- Rename, archive, restore, delete, fork, and export sessions
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 重命名、归档、恢复、删除、复制、分叉与导出会话
- Rename, archive, restore, delete, copy, fork, and export sessions
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 为会话选择模型、工作模式、记忆模式与权限模式
- Choose the model, work mode, memory mode, and permission mode per session
- **由专用智能体工具控制 / Delegated Agent tool** · `FrontendWorkbench` / `Read` / `Write` / `Edit` · 让创造模式扩展客户端界面与运行时能力
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
"catalogDigest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
"catalogDigest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
"count": 216,
"reasonCounts": {
"externalAuth": 4,
Expand All @@ -13,8 +13,8 @@
{
"capabilityId": "feature.ai-assistant",
"itemId": "session-lifecycle",
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
"reasonCode": "visualSelection",
"reasonZh": "这些操作依赖选中的会话、当前运行状态或删除确认;智能体可准确打开会话界面,但不把新建会话冒充为完整生命周期控制。",
"reasonEn": "These actions depend on the selected session, its live state, or deletion confirmation; the Agent can open the exact session UI without pretending that session creation controls the full lifecycle.",
Expand All @@ -27,6 +27,7 @@
"command:archive_session",
"command:restore_session",
"command:delete_session",
"command:copy_session",
"command:fork_session",
"command:export_session_transcript",
"command:archive_all_sessions",
Expand Down
24 changes: 20 additions & 4 deletions docs/interactive-capabilities/technical/tauri-command-map.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schemaVersion": 2,
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
"catalogDigest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
"commandCount": 673,
"catalogDigest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
"commandCount": 674,
"coverage": {
"commandCount": 673,
"documentedCommandCount": 625,
"commandCount": 674,
"documentedCommandCount": 626,
"implementationCommandCount": 48,
"implementationDigest": "f6d38a24a70708988cb47ada81d07eccf0668684e8734c9ee5155b9ffa7e3db8"
},
Expand Down Expand Up @@ -1644,6 +1644,22 @@
"signature": "fn control_deep_review_queue( request: ControlDeepReviewQueueRequest, ) -> Result<(), String>",
"remoteWorkspacePolicy": "LegacyUnaudited"
},
{
"id": "copy_session",
"moduleId": "session",
"capabilityId": "feature.ai-assistant",
"capabilityIds": [
"feature.ai-assistant"
],
"documentedItemIds": [
"feature.ai-assistant:session-lifecycle"
],
"visibility": "documented",
"rustPath": "copy_session",
"sourceFile": "src/apps/desktop/src/api/session_api.rs",
"signature": "fn copy_session( request: CopySessionRequest, runtime: State<'_, DesktopRuntimeContext>, ) -> Result<CopySessionResponse, String>",
"remoteWorkspacePolicy": "RemoteRouted"
},
{
"id": "create_acp_flow_session",
"moduleId": "acp_client",
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ For session storage directory access, run
for session storage directory resolution (these tests do not launch a file manager),
and `cargo test -p openbitfun-desktop --lib remote_workspace_policy` after registration changes.

For whole-session copy routing, run
`cargo test --locked -p openbitfun-desktop --lib whole_session_copy_uses_the_latest_persisted_turn_fork`.
After command registration changes, also run the `remote_workspace_policy` filter.

For model selector request compatibility and configuration effect routing:

```bash
Expand Down
34 changes: 26 additions & 8 deletions src/apps/desktop/src/api/session_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,7 @@ fn default_include_hidden_subagents() -> bool {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForkSessionRequest {
pub source_session_id: String,
/// Turn to copy the history through. Omitted means "copy the whole
/// session": the host then forks at the session's latest persisted turn.
///
/// The field stays on the wire for existing callers and older frontends.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_turn_id: Option<String>,
pub source_turn_id: String,
#[serde(default)]
pub workspace_id: Option<String>,
#[serde(default)]
Expand All @@ -258,6 +253,14 @@ pub struct ForkSessionRequest {

pub type ForkSessionResponse = SessionBranchResult;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopySessionRequest {
pub source_session_id: String,
pub workspace_id: String,
}

pub type CopySessionResponse = SessionBranchResult;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveSessionRequest {
pub session_id: String,
Expand Down Expand Up @@ -728,8 +731,7 @@ pub async fn load_persisted_session_metadata(
result
}

/// Forks a session through a selected turn, or copies the whole session when
/// `source_turn_id` is omitted.
/// Forks a session through the caller's selected persisted turn.
#[tauri::command]
pub async fn fork_session(
request: ForkSessionRequest,
Expand All @@ -751,6 +753,22 @@ pub async fn fork_session(
.map_err(|error| format!("Failed to fork session: {}", desktop_session_error(error)))
}

/// Copies the whole session through its latest persisted turn.
#[tauri::command]
pub async fn copy_session(
request: CopySessionRequest,
runtime: State<'_, DesktopRuntimeContext>,
) -> Result<CopySessionResponse, String> {
runtime
.session_application()
.copy_session(
desktop_session_scope(Some(request.workspace_id), String::new(), None, None),
request.source_session_id,
)
.await
.map_err(|error| format!("Failed to copy session: {}", desktop_session_error(error)))
}

#[tauri::command]
pub async fn archive_session(
request: ArchiveSessionRequest,
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,7 @@ pub async fn run() {
touch_session_activity,
load_persisted_session_metadata,
fork_session,
copy_session,
archive_session,
unarchive_session,
archive_all_sessions,
Expand Down
23 changes: 14 additions & 9 deletions src/apps/desktop/src/runtime/session_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,30 +621,35 @@ impl DesktopSessionApplication {
Ok(report)
}

/// Copies a session into a new independent session.
///
/// `source_turn_id` selects a prefix of the source history. Omitting it
/// copies the whole session, which is the runtime's latest-persisted-turn
/// fork (`AgentSessionForkRequest`); the Desktop "Copy session" action uses
/// that form so the controller never has to load a session's turns just to
/// ask for a copy.
/// Forks a session through the caller's selected persisted turn.
pub(crate) async fn fork_session(
&self,
request: DesktopSessionScopeRequest,
source_session_id: String,
source_turn_id: Option<String>,
source_turn_id: String,
) -> DesktopSessionApplicationResult<SessionBranchResult> {
let scope = self.resolved_scope(request).await?;
self.ensure_runtime_ownership(&scope)?;
fork_session_in_scope(
&self.agent_runtime,
scope,
source_session_id,
source_turn_id,
Some(source_turn_id),
)
.await
}

/// Copies the whole session using the existing runtime-owned fork algorithm.
pub(crate) async fn copy_session(
&self,
request: DesktopSessionScopeRequest,
source_session_id: String,
) -> DesktopSessionApplicationResult<SessionBranchResult> {
let scope = self.resolved_scope(request).await?;
self.ensure_runtime_ownership(&scope)?;
fork_session_in_scope(&self.agent_runtime, scope, source_session_id, None).await
}

pub(crate) async fn set_session_archived(
&self,
request: DesktopSessionScopeRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"title": "OpenBitFun Playbook",
"origin": "https://playbook.openbitfun.com",
"source": "src/shared/interactive-capabilities/catalog.json",
"digest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
"digest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
"ownerDigest": "c989ace53614c2a526fefe64816fdd6895ec71edcc28f44b6f80d1edb75bb33e",
"searchAcceptance": [
{
Expand Down Expand Up @@ -19435,8 +19435,8 @@
},
{
"id": "session-lifecycle",
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
"control": {
"kind": "open",
"reasonCode": "visualSelection",
Expand Down Expand Up @@ -19739,8 +19739,8 @@
"Safely customize the running OpenBitFun frontend in Creative mode",
"打开并新建一个 AI 会话",
"Open and create a new AI session",
"重命名、归档、恢复、删除、分叉与导出会话",
"Rename, archive, restore, delete, fork, and export sessions",
"重命名、归档、恢复、删除、复制、分叉与导出会话",
"Rename, archive, restore, delete, copy, fork, and export sessions",
"为会话选择模型、工作模式、记忆模式与权限模式",
"Choose the model, work mode, memory mode, and permission mode per session",
"让创造模式扩展客户端界面与运行时能力",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"digest": "fnv1a64:c96edb696f388992",
"digest": "fnv1a64:e2b40d572990839b",
"retiredCommandPrefixes": [
{
"prefix": "lsp_",
Expand All @@ -27,6 +27,7 @@
"tool_catalog",
"chat_mcp_catalog_v1",
"workspace_id_references_v1",
"whole_session_copy_v1",
"user_question_response",
"user_question_interaction_v1"
],
Expand All @@ -49,6 +50,7 @@
"tool_catalog",
"chat_mcp_catalog_v1",
"workspace_id_references_v1",
"whole_session_copy_v1",
"user_question_response",
"user_question_interaction_v1"
],
Expand Down Expand Up @@ -1272,6 +1274,18 @@
"reason": "the CLI peer host has no handler for this command"
}
},
{
"id": "copy_session",
"surface": "tauri_command",
"remoteWorkspace": "RemoteRouted",
"peer": {
"kind": "proxied"
},
"cliPeer": {
"kind": "unsupported",
"reason": "the CLI peer host has no handler for this command"
}
},
{
"id": "create_acp_flow_session",
"surface": "tauri_command",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ pub enum PeerHostCapability {
ChatMcpCatalogV1,
/// Workspace references can carry the owning host's persisted ID.
WorkspaceIdReferencesV1,
/// `copy_session` copies a whole session without a caller-selected turn id.
WholeSessionCopyV1,
/// `submit_user_answers` is implemented for Runtime-owned questions.
UserQuestionResponse,
/// First human interaction disables the unattended question timeout.
Expand Down Expand Up @@ -85,6 +87,7 @@ impl PeerHostCapability {
Self::ToolCatalog,
Self::ChatMcpCatalogV1,
Self::WorkspaceIdReferencesV1,
Self::WholeSessionCopyV1,
Self::UserQuestionResponse,
Self::UserQuestionInteractionV1,
];
Expand All @@ -110,6 +113,7 @@ impl PeerHostCapability {
Self::ToolCatalog => "tool_catalog",
Self::ChatMcpCatalogV1 => "chat_mcp_catalog_v1",
Self::WorkspaceIdReferencesV1 => "workspace_id_references_v1",
Self::WholeSessionCopyV1 => "whole_session_copy_v1",
Self::UserQuestionResponse => "user_question_response",
Self::UserQuestionInteractionV1 => "user_question_interaction_v1",
}
Expand Down Expand Up @@ -221,5 +225,10 @@ mod tests {
assert!(!cli.contains_key("miniapp_agent_context_files_v1"));
assert!(!cli.contains_key("product_control_native_v1"));
assert!(!cli.contains_key("product_control_presentation_v1"));
assert!(!cli.contains_key("whole_session_copy_v1"));
assert_eq!(
capability_map(PeerHostKind::Desktop).get("whole_session_copy_v1"),
Some(&Value::Bool(true))
);
}
}
15 changes: 15 additions & 0 deletions src/crates/contracts/product-domains/src/remote_surface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,21 @@ mod tests {
}
}

#[test]
fn whole_session_copy_runs_on_desktop_and_is_explicitly_unsupported_on_cli() {
let copy = operation("copy_session").unwrap();
assert_eq!(copy.remote_workspace, RemoteWorkspaceStance::Routed);
assert_eq!(copy.peer, PeerStance::Proxied);
assert_eq!(
peer_host_verdict("copy_session", PeerHostKind::Desktop),
PeerHostVerdict::Execute
);
assert!(matches!(
peer_host_verdict("copy_session", PeerHostKind::Cli),
PeerHostVerdict::Refuse(PeerRefusal::HostUnsupported { .. })
));
}

#[test]
fn remote_workspace_stance_keeps_historical_policy_names() {
for (stance, expected) in [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ pub(super) const OPERATIONS: &[OperationDefinition] = &[
op("confirm_frontend_update", LocalOnly, ControllerLocal, REFUSED),
op("control_background_command", Unaudited, Proxied, CLI_NOT_IMPLEMENTED),
op("control_deep_review_queue", Unaudited, Proxied, CLI_NOT_IMPLEMENTED),
op("copy_session", Routed, Proxied, CLI_NOT_IMPLEMENTED),
op("create_acp_flow_session", Unaudited, Proxied, CLI_NOT_IMPLEMENTED),
op("create_assistant_workspace", Unaudited, Proxied, CLI_NOT_IMPLEMENTED),
op("create_control_conversation", Agnostic, Proxied, HANDLED),
Expand Down
Loading
Loading