From 2d1605e36e534e79c420b9a7a3d1cc7aa6ba664b Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 29 May 2026 22:16:25 +0800 Subject: [PATCH 1/3] docs(agent): add session transfer spec --- docs/features/agent-session-transfer/plan.md | 267 ++++++++++++++++++ docs/features/agent-session-transfer/spec.md | 209 ++++++++++++++ docs/features/agent-session-transfer/tasks.md | 24 ++ 3 files changed, 500 insertions(+) create mode 100644 docs/features/agent-session-transfer/plan.md create mode 100644 docs/features/agent-session-transfer/spec.md create mode 100644 docs/features/agent-session-transfer/tasks.md diff --git a/docs/features/agent-session-transfer/plan.md b/docs/features/agent-session-transfer/plan.md new file mode 100644 index 000000000..6659ae159 --- /dev/null +++ b/docs/features/agent-session-transfer/plan.md @@ -0,0 +1,267 @@ +# Implementation Plan - Agent Session Transfer + +## Current Code Notes + +- `DeepChatAgentsSettings.vue` deletes custom DeepChat agents through + `configPresenter.deleteDeepChatAgent(form.id)` after `window.confirm`. +- `AcpSettings.vue` deletes manual ACP agents through `configPresenter.removeManualAcpAgent(agent.id)` + after `window.confirm`. +- `AgentRepository.deleteDeepChatAgent` currently reassigns all `new_sessions.agent_id` values from + the deleted custom DeepChat agent to built-in `deepchat`. +- `AgentRepository.removeManualAcpAgent` deletes only the agent record. +- `NewSessionsTable` already supports list/page filtering by `agentId` and has a batch + `reassignAgentId(fromAgentId, toAgentId)`, but it does not expose a precise single-session + ownership update or impact counts. +- `AgentSessionPresenter` is the correct place to check runtime state, delete sessions with their + child subagents, emit session-list updates, and coordinate ACP workdir/session cleanup. +- `AgentRuntimePresenter` caches session agent ids in memory, so ownership moves need a runtime hook, + not just a database update. + +## Architecture + +### Shared Contracts + +Add typed route contracts in `src/shared/contracts/routes/sessions.routes.ts`: + +- `sessions.getAgentTransferImpact` + - input: `{ agentId: string }` + - output: `{ impact: AgentTransferImpact }` +- `sessions.moveAgentSessions` + - input: `{ fromAgentId: string; toAgentId: string; acpWorkdirBySessionId?: Record }` + - output: `{ movedSessionIds: string[]; deletedDraftSessionIds: string[]; skipped: AgentTransferSkip[] }` +- `sessions.deleteAgentSessions` + - input: `{ agentId: string }` + - output: `{ deletedSessionIds: string[] }` +- `sessions.moveSessionToAgent` + - input: `{ sessionId: string; toAgentId: string; acpWorkdir?: string }` + - output: `{ session: SessionWithState }` + +Proposed shared shape: + +```ts +type AgentTransferImpact = { + agentId: string + totalSessions: number + regularSessions: number + subagentSessions: number + emptyDrafts: number + movableSessions: number + blockedSessions: number + samples: Array<{ + id: string + title: string + sessionKind: 'regular' | 'subagent' + isDraft: boolean + projectDir: string | null + status: 'idle' | 'generating' | 'error' + blockReason?: 'active' | 'pending-input' | 'missing-target-workdir' + }> +} +``` + +Keep `config.listAgents` as the source for enabled target-agent options. + +### Presenter Responsibilities + +`AgentSessionPresenter`: + +- `getAgentTransferImpact(agentId)` + - List `new_sessions` with `{ agentId, includeSubagents: true }`. + - Treat drafts with no message ids as empty drafts. + - Use `getSessionState` and pending-input inspection to classify movable vs blocked. + - Return a small sample list for the dialog, not the full history. +- `moveAgentSessions(fromAgentId, toAgentId, options)` + - Validate both agents exist and are not the same. + - Recompute impact server-side immediately before mutation. + - Refuse the entire operation when any non-empty related session is blocked. + - Delete empty drafts for the source agent. + - Move each non-empty related session through the same internal helper used by + `moveSessionToAgent`. +- `moveSessionToAgent(sessionId, toAgentId, options)` + - Only allow regular sessions from the public chat-level route. + - Refuse active/generating sessions. + - Resolve target runtime defaults: + - DeepChat target: target agent config merged with built-in DeepChat defaults, then app default + model fallback. + - ACP target: provider `acp`, model id target agent id, permission `full_access`, no DeepChat + disabled-tool list. + - Update `new_sessions.agent_id` and any target-specific session fields in one logical operation. + - Update the DeepChat runtime's session-agent cache and provider/model/generation settings. + - Clear stale ACP binding for the previous ACP agent when moving away from ACP. + - For ACP targets, ensure/set workdir using the session project dir or the explicit `acpWorkdir`. + - Emit `SESSION_EVENTS.LIST_UPDATED` / typed `sessions.updated`. +- `deleteAgentSessions(agentId)` + - Recompute impact. + - Refuse active sessions. + - Delete related sessions with existing `deleteSessionInternal`, which already handles child + sessions, message cleanup, permissions, skills, search documents, and ACP cleanup. + +`AgentRuntimePresenter`: + +- Add an optional implementation method such as `setSessionAgentContext(sessionId, context)` to + `IAgentImplementation`. +- The DeepChat implementation updates `sessionAgentIds`, provider/model, generation settings, + disabled tool cache, project dir cache, and invalidates system prompt/tool profile caches without + deleting messages. +- It must reject generating sessions. + +`NewSessionsTable` / `NewSessionManager`: + +- Add a precise `updateAgentId(sessionId, agentId)` helper. +- Keep `reassignAgentId` as a low-level fallback only; the transfer path should move session by + session so runtime state and ACP cleanup stay correct. +- Consider `countByAgent(agentId, includeSubagents)` for fast summary, but correctness can start with + `list({ agentId, includeSubagents: true })`. + +`AgentRepository` / `ConfigPresenter`: + +- Change custom DeepChat deletion so it no longer silently reassigns sessions to built-in DeepChat. + The UI should call the session move/delete route first, then delete the agent. +- Manual ACP deletion can keep removing only the agent record, because the UI/session route will have + already moved or deleted related sessions. +- Keep a defensive fallback in the delete methods: if sessions still exist for the source agent, + return `false` or throw a clear error instead of silently orphaning or reassigning them. + +### Renderer + +Create a reusable `AgentDeleteImpactDialog.vue` under settings components or a shared settings dialog +folder. It should be used by: + +- `DeepChatAgentsSettings.vue` +- `AcpSettings.vue` manual custom-agent section + +The dialog fetches: + +- impact via `SessionClient.getAgentTransferImpact(agentId)` +- target options via `ConfigClient.listAgents()`, filtered to enabled agents except source + +It submits: + +- move path: `SessionClient.moveAgentSessions(...)`, then existing agent deletion +- delete path: `SessionClient.deleteAgentSessions(agentId)`, then existing agent deletion + +Add chat-level move UI in `ChatTopBar.vue` or the existing status/agent menu area: + +- Show only for active regular sessions. +- Disable while the session status is generating. +- Use `SessionClient.moveSessionToAgent(sessionId, targetAgentId, acpWorkdir?)`. +- Update `useSessionStore` with the returned session and let existing selected-agent sync run. + +## Event Flow + +Delete with move: + +```text +Settings delete button + -> SessionClient.getAgentTransferImpact(agentId) + -> AgentDeleteImpactDialog opens + -> user chooses target agent + -> SessionClient.moveAgentSessions(fromAgentId, toAgentId) + -> AgentSessionPresenter moves sessions and emits sessions.updated + -> ConfigPresenter deletes source agent and emits config.agents.changed + -> settings reloads agents, session store refreshes affected sessions +``` + +Delete with related chats removed: + +```text +Settings delete button + -> impact dialog + -> user chooses "Delete chats with this Agent" + -> SessionClient.deleteAgentSessions(agentId) + -> ConfigPresenter deletes source agent + -> session store receives deleted session ids / list update +``` + +Single session move: + +```text +Chat agent menu + -> user selects target agent + -> SessionClient.moveSessionToAgent(sessionId, targetAgentId) + -> AgentSessionPresenter updates session ownership/runtime context + -> sessions.updated(reason: updated) + -> Session store upserts returned session and selected agent syncs to target +``` + +## Data Rules + +- Preserve: + - `new_sessions.id`, title, pin state, project dir, parent relation + - deepchat messages, assistant blocks, files, traces, tape entries + - search documents and usage stats + - session summary/compaction state where still meaningful +- Reset or recompute: + - target agent id + - provider/model for future turns + - DeepChat generation defaults for target DeepChat agents + - disabled tools from the target DeepChat agent + - ACP external session id and bound process state +- Delete: + - empty drafts during delete-agent flow + - related sessions only when the user explicitly picks the destructive option + +## ACP Workdir Handling + +- If moving to ACP and `session.projectDir` is non-empty, use it as the target ACP workdir. +- If moving a single chat to ACP and there is no project dir, ask for/select a workdir in the dialog. +- If batch-moving during agent deletion and some sessions lack project dirs, block with a clear list + and ask the user to either choose a shared workdir or delete those chats. +- Do not reuse the previous ACP `session_id`; call the existing ACP preparation path so the next turn + starts a fresh target-agent ACP session. + +## Compatibility + +- Existing sessions assigned to removed manual ACP agents are not fixed automatically by this feature + unless the source agent still exists at deletion time. A later repair utility can scan orphaned + `new_sessions.agent_id` values. +- Existing custom DeepChat deletion behavior changes from implicit fallback to explicit user choice. + This is intentional and should be called out in release notes. +- No schema migration is required for the first increment if `new_sessions.agent_id` is updated via a + new helper. + +## Test Strategy + +Main tests: + +- `agentRepository.test.ts` + - Custom DeepChat delete refuses when sessions remain, or no longer silently reassigns. + - Manual ACP delete remains safe after sessions are moved/deleted. +- `agentSessionPresenter.test.ts` + - Impact summary counts regular/subagent/draft/blocked sessions. + - Batch move DeepChat -> DeepChat applies target ownership and model defaults. + - Batch move ACP -> DeepChat clears ACP binding and keeps messages. + - Move DeepChat -> ACP requires workdir and sets provider/model to ACP target. + - Active/generating sessions block move and delete. + - `deleteAgentSessions` deletes related sessions through existing recursive cleanup. +- `agentRuntimePresenter.test.ts` + - Session agent context update refreshes cached agent id and invalidates prompt/tool caches. + - Generating sessions reject context transfer. + +Renderer tests: + +- `DeepChatAgentsSettings.test.ts` + - Delete opens impact dialog. + - Move path calls session move before agent delete. + - Delete path calls session delete before agent delete. +- `AcpSettings` test coverage for manual ACP delete impact dialog. +- Chat component/store tests for single-session move and disabled active state. + +Quality gates after implementation: + +- `pnpm run format` +- `pnpm run i18n` +- `pnpm run lint` +- Targeted Vitest suites for touched main and renderer modules + +## Risks + +- Runtime cache drift is the main risk. Mitigation: transfer through AgentSessionPresenter and add a + runtime-level context update method. +- ACP sessions may have provider-specific expectations about session continuity. Mitigation: only move + idle sessions and start a fresh target ACP binding. +- Batch deletion is destructive. Mitigation: in-app dialog, explicit destructive option, and tests that + verify move is the default/safe path. +- Applying target DeepChat defaults can surprise users who expected current model settings to remain. + Mitigation: dialog copy states that future replies use the target agent; future enhancement can add + "keep current model/settings". diff --git a/docs/features/agent-session-transfer/spec.md b/docs/features/agent-session-transfer/spec.md new file mode 100644 index 000000000..4a8c63884 --- /dev/null +++ b/docs/features/agent-session-transfer/spec.md @@ -0,0 +1,209 @@ +# Agent Session Transfer + +## Context + +[Issue #1705](https://github.com/ThinkInAIXYZ/deepchat/issues/1705) reports that deleting an +agent can make the user's valuable work feel lost because the app does not explain what happens to +that agent's chats. The current implementation is inconsistent: + +- Custom DeepChat agent deletion asks for a simple browser confirm, then reassigns + `new_sessions.agent_id` to the built-in `deepchat`. +- Manual ACP agent deletion asks for a simple browser confirm, then removes the agent record without + reassigning sessions. Sessions that still point at the removed ACP agent can become unavailable in + the session list. + +The feature should turn agent deletion from an opaque destructive action into an explicit choice: +move/import related conversations to another agent, or delete those conversations together with the +agent. + +## User Need + +Users need to understand and control what happens to chats owned by an agent before deleting that +agent. They also need a normal chat-level way to move an idle conversation to another agent when the +conversation history is useful but the next turns should use a different agent. + +## Goals + +- Show a structured delete-agent dialog whenever a deletable DeepChat or manual ACP agent has related + sessions. +- Offer two clear outcomes in that dialog: + - Move related sessions to another enabled agent, then delete the source agent. + - Delete related sessions, then delete the source agent. +- Allow regular idle sessions to move from one agent to another outside the delete flow. +- Support DeepChat-to-DeepChat, DeepChat-to-ACP, ACP-to-DeepChat, and ACP-to-ACP moves for idle + sessions by preserving conversation history and reinitializing the target agent runtime for future + turns. +- Keep empty draft sessions from blocking deletion. + +## Non-goals + +- No moving sessions while a turn is generating, waiting for tool approval, or otherwise active. +- No automatic migration for registry ACP uninstall in the first increment. Manual ACP delete and + custom DeepChat delete are the user-visible delete paths covered first. +- No cross-agent move for subagent sessions from the manual chat action in the first increment. + Subagent sessions are included in delete-agent impact handling because they can still reference the + deleted agent. +- No deep copy/duplicate UI in the first increment. The first shipped action is move/import, not + "duplicate and keep the original under the old agent". +- No attempt to preserve an ACP provider's external session id when moving to another agent. Target + ACP sessions start with a fresh ACP binding. + +## Terminology + +- **Move/import**: keep the same DeepChat session id and stored messages, change the owning + `agent_id`, and make future turns use the target agent. +- **Related sessions**: rows in `new_sessions` whose `agent_id` is the agent being deleted, including + regular sessions, subagent sessions, and drafts. +- **Importable sessions**: related sessions that are idle and can be safely re-bound to a target + agent. +- **Empty drafts**: draft rows without messages. These may be deleted during source-agent deletion + without being offered as valuable chat history. + +## User Stories + +1. As a user deleting a custom DeepChat agent, I can see how many chats will be affected before I + confirm deletion. +2. As a user deleting a manual ACP agent, I can move that agent's finished chats to another agent so + they remain visible and usable. +3. As a user who does not want to keep related chats, I can explicitly delete those chats together + with the agent. +4. As a user viewing an idle regular conversation, I can move it to a different agent from the chat + UI, then continue the conversation with the target agent. +5. As a user with an active conversation, I am told to stop or wait before moving the conversation. + +## Acceptance Criteria + +1. Deleting a custom DeepChat or manual ACP agent with related non-empty sessions opens an in-app + dialog rather than `window.confirm`. +2. The delete dialog shows counts for regular sessions, subagent sessions, empty drafts, and sessions + that cannot currently be moved because they are active. +3. The primary safe action is "Move chats to..." and requires selecting an enabled target agent that + is not the source agent. +4. The destructive action is "Delete chats and agent"; it clearly states that related chats will be + removed. +5. If the source agent has no non-empty related sessions, deletion can use a shorter in-app confirm + that states there are no chats to move. +6. Moving to a DeepChat agent applies the target agent's runtime defaults for future turns while + preserving existing messages, attachments, search documents, tape entries, and title. +7. Moving to an ACP agent sets future turns to provider `acp` and model id equal to the target agent + id. If no project/workdir is available, the UI asks for one or blocks the move with a clear error. +8. Active or generating sessions cannot move. The dialog lists the blocked count and disables the + move/delete completion until those sessions are stopped or finish. +9. After a successful delete-agent move, the source agent is removed and moved sessions appear under + the target agent in the session list without an app restart. +10. After a successful chat-level move, the active session remains open, the selected agent syncs to + the target agent, and the next user message uses the target agent. +11. All new user-facing text uses i18n keys. +12. Tests cover impact summary, DeepChat deletion with move, manual ACP deletion with move, explicit + delete of related sessions, chat-level move, and active-session blocking. + +## UX States + +### Delete Agent With Movable Chats + +```text ++------------------------------------------------------------+ +| Delete Agent | +| | +| Agent: Code Reviewer | +| | +| This agent has conversations attached to it. Choose how | +| DeepChat should handle them before the agent is deleted. | +| | +| Impact | +| Regular chats 12 | +| Subagent chats 3 | +| Empty drafts 2 | +| Currently active 0 | +| | +| What should happen to these chats? | +| | +| (o) Move chats to another Agent | +| Target Agent | +| [ DeepChat v ] | +| | +| Future replies will use the target Agent. Existing | +| messages and files stay in the same chats. | +| | +| ( ) Delete chats with this Agent | +| Related chats and their local files will be removed. | +| | +| [ Cancel ] [ Move & Delete ] | ++------------------------------------------------------------+ +``` + +### Delete Agent With Active Chats + +```text ++------------------------------------------------------------+ +| Delete Agent | +| | +| Agent: Claude Code | +| | +| 2 related chats are still active. Stop or wait for those | +| chats before deleting this agent. | +| | +| Impact | +| Regular chats 5 | +| Subagent chats 0 | +| Empty drafts 1 | +| Currently active 2 | +| | +| [ View Active Chats ] [ Close ] | ++------------------------------------------------------------+ +``` + +### Delete Agent With No Chats + +```text ++----------------------------------------------+ +| Delete Agent | +| | +| Delete "Scratch Agent"? | +| No conversations are attached to this agent. | +| | +| [ Cancel ] [ Delete ] | ++----------------------------------------------+ +``` + +### Chat-Level Move + +```text +Chat Top Bar ++------------------------------------------------------------+ +| Project notes [Agent: DeepChat v] | ++------------------------------------------------------------+ + +Agent menu ++-----------------------------------------------+ +| Move conversation to Agent | +| | +| Current: DeepChat | +| Target | +| [ Code Reviewer v ] | +| | +| Existing messages stay here. Future replies | +| will use Code Reviewer. | +| | +| [ Cancel ] [ Move ] | ++-----------------------------------------------+ +``` + +## Constraints + +- Follow the existing Presenter pattern: session ownership and transfer belongs in + `AgentSessionPresenter`; agent record deletion remains in `ConfigPresenter` / + `AgentRepository`. +- New renderer-main APIs should use typed routes and `renderer/api/*Client` rather than adding new + direct `useLegacyPresenter()` usage. +- Existing `new_sessions` rows, message tables, tape tables, files, search documents, and usage stats + are user data and must not be dropped during move/import. +- The transfer flow must update both persisted state and in-memory runtime caches, otherwise hooks and + the next message can still report the old agent id. +- ACP transfer must treat `acp_sessions` as target-agent-specific because it is keyed by + `(conversation_id, agent_id)`. + +## Open Questions + +None for the first increment. A future duplicate/copy feature can be specified separately after the +move/import behavior is shipped and tested. diff --git a/docs/features/agent-session-transfer/tasks.md b/docs/features/agent-session-transfer/tasks.md new file mode 100644 index 000000000..74ba256f2 --- /dev/null +++ b/docs/features/agent-session-transfer/tasks.md @@ -0,0 +1,24 @@ +# Tasks - Agent Session Transfer + +- [ ] Contracts: add typed session transfer routes and shared schemas in `sessions.routes.ts`. +- [ ] Client: expose `getAgentTransferImpact`, `moveAgentSessions`, `deleteAgentSessions`, and + `moveSessionToAgent` from `SessionClient`. +- [ ] SQLite/session manager: add precise `updateAgentId(sessionId, agentId)` and any small counting + helpers needed for impact summaries. +- [ ] Runtime: add a DeepChat runtime method to update session agent context without deleting + messages; reject generating sessions and invalidate agent-dependent caches. +- [ ] Main presenter: implement `AgentSessionPresenter` impact summary, batch move, single-session + move, and delete-by-agent flows. +- [ ] ACP handling: clear stale source ACP bindings and prepare/set target ACP workdir for ACP + targets. +- [ ] Agent deletion safety: remove silent DeepChat fallback reassignment and prevent deleting an + agent while sessions still point at it. +- [ ] Renderer dialog: build `AgentDeleteImpactDialog.vue` with move/delete states, target-agent + selection, blocked-session messaging, loading, and error states. +- [ ] Settings integration: replace `window.confirm` deletion in `DeepChatAgentsSettings.vue` and + manual-agent deletion in `AcpSettings.vue`. +- [ ] Chat-level move: add the regular idle conversation move action and store/client integration. +- [ ] i18n: add English and Chinese strings first, then run the repository i18n workflow for other + locales. +- [ ] Tests: add main presenter/runtime/repository coverage and renderer dialog/store coverage. +- [ ] Validation: run `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, and targeted tests. From 4b09d6bdab68d5324b033c33b3ee92dfcd7840a2 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 29 May 2026 22:22:53 +0800 Subject: [PATCH 2/3] docs(agent): refine transfer ux --- docs/features/agent-session-transfer/plan.md | 55 ++++++- docs/features/agent-session-transfer/spec.md | 141 ++++++++++++------ docs/features/agent-session-transfer/tasks.md | 8 +- 3 files changed, 150 insertions(+), 54 deletions(-) diff --git a/docs/features/agent-session-transfer/plan.md b/docs/features/agent-session-transfer/plan.md index 6659ae159..dcbfe4526 100644 --- a/docs/features/agent-session-transfer/plan.md +++ b/docs/features/agent-session-transfer/plan.md @@ -16,6 +16,9 @@ child subagents, emit session-list updates, and coordinate ACP workdir/session cleanup. - `AgentRuntimePresenter` caches session agent ids in memory, so ownership moves need a runtime hook, not just a database update. +- `ChatTopBar.vue` already has a right-side `...` dropdown for writable sessions. Its current order + is pin/unpin, clear messages, separator, delete. The session move entry should be inserted between + pin/unpin and clear messages. ## Architecture @@ -124,13 +127,28 @@ Keep `config.listAgents` as the source for enabled target-agent options. ### Renderer -Create a reusable `AgentDeleteImpactDialog.vue` under settings components or a shared settings dialog -folder. It should be used by: +Create a reusable transfer dialog surface, likely `AgentTransferDialog.vue` plus a thin +delete-agent wrapper. It should support both one-shot agent deletion migration and single-session +movement. + +For delete-agent usage, mount it from: - `DeepChatAgentsSettings.vue` - `AcpSettings.vue` manual custom-agent section -The dialog fetches: +For chat-level usage, mount it from `ChatTopBar.vue` and open it from the right-side `...` menu item +placed between pin/unpin and clear messages: + +```text +DropdownMenuContent + Pin / Unpin + Move conversation + Clear messages + separator + Delete +``` + +The delete-agent dialog fetches: - impact via `SessionClient.getAgentTransferImpact(agentId)` - target options via `ConfigClient.listAgents()`, filtered to enabled agents except source @@ -140,10 +158,27 @@ It submits: - move path: `SessionClient.moveAgentSessions(...)`, then existing agent deletion - delete path: `SessionClient.deleteAgentSessions(agentId)`, then existing agent deletion -Add chat-level move UI in `ChatTopBar.vue` or the existing status/agent menu area: +The chat-level dialog fetches the same target options and submits: + +- move path: `SessionClient.moveSessionToAgent(sessionId, targetAgentId, acpWorkdir?)` + +Dialog layout requirements: + +- Use a viewport-aware max height, for example + `max-h-[min(720px,calc(100vh-2rem))]` on desktop and `max-h-[calc(100vh-1rem)]` on narrow + screens. +- Keep the header and footer outside the scrolling region so the title and actions remain visible. +- Put impact summaries, affected chat samples, explanatory copy, target picker, and ACP workdir + controls in an internal `overflow-y-auto` body. +- Prefer a single-column mobile layout; only use side-by-side summary/details areas on wider screens. +- Ensure long agent names, session titles, and project paths truncate or wrap without widening the + dialog. +- Use a loading state inside the body, not a full-window blocker. + +Chat-level move behavior: - Show only for active regular sessions. -- Disable while the session status is generating. +- Disable while the session status is generating or while a required ACP workdir is missing. - Use `SessionClient.moveSessionToAgent(sessionId, targetAgentId, acpWorkdir?)`. - Update `useSessionStore` with the returned session and let existing selected-agent sync run. @@ -176,7 +211,7 @@ Settings delete button Single session move: ```text -Chat agent menu +ChatTopBar right-side ... menu -> user selects target agent -> SessionClient.moveSessionToAgent(sessionId, targetAgentId) -> AgentSessionPresenter updates session ownership/runtime context @@ -245,7 +280,13 @@ Renderer tests: - Move path calls session move before agent delete. - Delete path calls session delete before agent delete. - `AcpSettings` test coverage for manual ACP delete impact dialog. -- Chat component/store tests for single-session move and disabled active state. +- `ChatTopBar` tests: + - `Move conversation` appears between pin/unpin and clear messages. + - Selecting it opens the transfer dialog. + - The entry is disabled or unavailable for read-only/subagent/active sessions. +- Dialog responsiveness tests should assert the shell has a viewport max height and the detail body + owns the scroll region. +- Store/client tests for single-session move and disabled active state. Quality gates after implementation: diff --git a/docs/features/agent-session-transfer/spec.md b/docs/features/agent-session-transfer/spec.md index 4a8c63884..2db06c5a1 100644 --- a/docs/features/agent-session-transfer/spec.md +++ b/docs/features/agent-session-transfer/spec.md @@ -30,6 +30,9 @@ conversation history is useful but the next turns should use a different agent. - Move related sessions to another enabled agent, then delete the source agent. - Delete related sessions, then delete the source agent. - Allow regular idle sessions to move from one agent to another outside the delete flow. +- Expose exactly two first-increment migration entry points: + - One-shot migration while deleting an agent. + - A chat detail action from the active conversation's top-right `...` menu. - Support DeepChat-to-DeepChat, DeepChat-to-ACP, ACP-to-DeepChat, and ACP-to-ACP moves for idle sessions by preserving conversation history and reinitializing the target agent runtime for future turns. @@ -93,8 +96,13 @@ conversation history is useful but the next turns should use a different agent. the target agent in the session list without an app restart. 10. After a successful chat-level move, the active session remains open, the selected agent syncs to the target agent, and the next user message uses the target agent. -11. All new user-facing text uses i18n keys. -12. Tests cover impact summary, DeepChat deletion with move, manual ACP deletion with move, explicit +11. The chat-level move entry is in the active conversation top bar's right-side `...` menu, placed + between "Pin/Unpin" and "Clear messages". +12. Transfer dialogs are responsive: they keep a viewport-aware maximum height, keep header/footer + actions visible, and scroll only the detailed body content when the impact list or help text is + long. +13. All new user-facing text uses i18n keys. +14. Tests cover impact summary, DeepChat deletion with move, manual ACP deletion with move, explicit delete of related sessions, chat-level move, and active-session blocking. ## UX States @@ -102,34 +110,39 @@ conversation history is useful but the next turns should use a different agent. ### Delete Agent With Movable Chats ```text -+------------------------------------------------------------+ -| Delete Agent | -| | -| Agent: Code Reviewer | -| | -| This agent has conversations attached to it. Choose how | -| DeepChat should handle them before the agent is deleted. | -| | -| Impact | -| Regular chats 12 | -| Subagent chats 3 | -| Empty drafts 2 | -| Currently active 0 | -| | -| What should happen to these chats? | -| | -| (o) Move chats to another Agent | -| Target Agent | -| [ DeepChat v ] | -| | -| Future replies will use the target Agent. Existing | -| messages and files stay in the same chats. | -| | -| ( ) Delete chats with this Agent | -| Related chats and their local files will be removed. | -| | -| [ Cancel ] [ Move & Delete ] | -+------------------------------------------------------------+ ++----------------------------------------------------------------+ +| Delete Agent | +| Agent: Code Reviewer | ++----------------------------------------------------------------+ +| This agent has conversations attached to it. Choose how | +| DeepChat should handle them before the agent is deleted. | +| | +| Impact | +| Regular chats 12 | +| Subagent chats 3 | +| Empty drafts 2 | +| Currently active 0 | +| | +| What should happen to these chats? | +| | +| (o) Move chats to another Agent | +| Target Agent | +| [ DeepChat v ] | +| | +| Future replies will use the target Agent. Existing | +| messages and files stay in the same chats. | +| | +| ( ) Delete chats with this Agent | +| Related chats and their local files will be removed. | +| | +| Recent affected chats | +| - Automation setup | +| - Code review workflow | +| - Release checklist | +| ... body scrolls when this area grows ... | ++----------------------------------------------------------------+ +| [ Cancel ] [ Move & Delete ] | ++----------------------------------------------------------------+ ``` ### Delete Agent With Active Chats @@ -170,23 +183,63 @@ conversation history is useful but the next turns should use a different agent. ```text Chat Top Bar ++----------------------------------------------------------------+ +| Project notes [share] [...] | ++----------------------------------------------------------------+ + +Top-right ... menu ++--------------------------------------+ +| Pin | +| Move conversation | +| Clear messages | +| ------------------------------------ | +| Delete | ++--------------------------------------+ + +Move dialog ++------------------------------------------------------------+ +| Move Conversation | +| Project notes | ++------------------------------------------------------------+ +| Current Agent | +| DeepChat | +| | +| Target Agent | +| [ Code Reviewer v ] | +| | +| Existing messages and files stay in this conversation. | +| Future replies will use the target Agent. | +| | +| If the target is ACP and this chat has no project folder, | +| this area asks for a workdir. Long details scroll here. | ++------------------------------------------------------------+ +| [ Cancel ] [ Move ] | ++------------------------------------------------------------+ +``` + +### Responsive Dialog Rules + +```text +Desktop / tablet ++------------------------------------------------------------+ +| Fixed header: title, source agent/session | ++------------------------------------------------------------+ +| Scroll body: impact, affected chat samples, target picker, | +| explanatory copy, ACP workdir selector when needed | +------------------------------------------------------------+ -| Project notes [Agent: DeepChat v] | +| Fixed footer: cancel + primary/destructive action | +------------------------------------------------------------+ -Agent menu -+-----------------------------------------------+ -| Move conversation to Agent | -| | -| Current: DeepChat | -| Target | -| [ Code Reviewer v ] | -| | -| Existing messages stay here. Future replies | -| will use Code Reviewer. | -| | -| [ Cancel ] [ Move ] | -+-----------------------------------------------+ +Narrow mobile ++--------------------------------------+ +| Fixed header | ++--------------------------------------+ +| Single-column scroll body | +| Controls keep full width | ++--------------------------------------+ +| Fixed footer | +| [ Cancel ] [ Move ] | ++--------------------------------------+ ``` ## Constraints diff --git a/docs/features/agent-session-transfer/tasks.md b/docs/features/agent-session-transfer/tasks.md index 74ba256f2..c001784ca 100644 --- a/docs/features/agent-session-transfer/tasks.md +++ b/docs/features/agent-session-transfer/tasks.md @@ -13,11 +13,13 @@ targets. - [ ] Agent deletion safety: remove silent DeepChat fallback reassignment and prevent deleting an agent while sessions still point at it. -- [ ] Renderer dialog: build `AgentDeleteImpactDialog.vue` with move/delete states, target-agent - selection, blocked-session messaging, loading, and error states. +- [ ] Renderer dialog: build a responsive transfer dialog with a viewport-aware max height, fixed + header/footer, internal scroll body, move/delete states, target-agent selection, + blocked-session messaging, loading, and error states. - [ ] Settings integration: replace `window.confirm` deletion in `DeepChatAgentsSettings.vue` and manual-agent deletion in `AcpSettings.vue`. -- [ ] Chat-level move: add the regular idle conversation move action and store/client integration. +- [ ] Chat-level move: add `Move conversation` to `ChatTopBar.vue`'s right-side `...` menu between + pin/unpin and clear messages, then wire it to the transfer dialog and store/client integration. - [ ] i18n: add English and Chinese strings first, then run the repository i18n workflow for other locales. - [ ] Tests: add main presenter/runtime/repository coverage and renderer dialog/store coverage. From 444115c6aed32ad97718fb52a88fef9aca950b69 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 29 May 2026 23:49:55 +0800 Subject: [PATCH 3/3] feat(agent): add session transfer flow --- docs/features/agent-session-transfer/plan.md | 48 +-- docs/features/agent-session-transfer/spec.md | 58 +-- docs/features/agent-session-transfer/tasks.md | 31 +- src/main/presenter/agentRepository/index.ts | 16 +- .../presenter/agentRuntimePresenter/index.ts | 49 +++ .../presenter/agentSessionPresenter/index.ts | 338 +++++++++++++++++- .../agentSessionPresenter/sessionManager.ts | 10 + .../sqlitePresenter/tables/newSessions.ts | 6 + src/main/routes/index.ts | 36 ++ src/renderer/api/SessionClient.ts | 33 ++ .../settings/components/AcpSettings.vue | 109 +++++- .../components/DeepChatAgentsSettings.vue | 95 ++++- .../components/agent/AgentTransferDialog.vue | 304 ++++++++++++++++ .../src/components/chat/ChatTopBar.vue | 71 ++++ src/renderer/src/i18n/da-DK/dialog.json | 47 ++- src/renderer/src/i18n/da-DK/thread.json | 3 +- src/renderer/src/i18n/de-DE/dialog.json | 45 +++ src/renderer/src/i18n/de-DE/thread.json | 3 +- src/renderer/src/i18n/en-US/dialog.json | 45 +++ src/renderer/src/i18n/en-US/thread.json | 1 + src/renderer/src/i18n/es-ES/dialog.json | 45 +++ src/renderer/src/i18n/es-ES/thread.json | 3 +- src/renderer/src/i18n/fa-IR/dialog.json | 45 +++ src/renderer/src/i18n/fa-IR/thread.json | 3 +- src/renderer/src/i18n/fr-FR/dialog.json | 45 +++ src/renderer/src/i18n/fr-FR/thread.json | 3 +- src/renderer/src/i18n/he-IL/dialog.json | 45 +++ src/renderer/src/i18n/he-IL/thread.json | 3 +- src/renderer/src/i18n/id-ID/dialog.json | 45 +++ src/renderer/src/i18n/id-ID/thread.json | 3 +- src/renderer/src/i18n/it-IT/dialog.json | 45 +++ src/renderer/src/i18n/it-IT/thread.json | 3 +- src/renderer/src/i18n/ja-JP/dialog.json | 45 +++ src/renderer/src/i18n/ja-JP/thread.json | 3 +- src/renderer/src/i18n/ko-KR/dialog.json | 45 +++ src/renderer/src/i18n/ko-KR/thread.json | 3 +- src/renderer/src/i18n/ms-MY/dialog.json | 45 +++ src/renderer/src/i18n/ms-MY/thread.json | 3 +- src/renderer/src/i18n/pl-PL/dialog.json | 45 +++ src/renderer/src/i18n/pl-PL/thread.json | 3 +- src/renderer/src/i18n/pt-BR/dialog.json | 45 +++ src/renderer/src/i18n/pt-BR/thread.json | 3 +- src/renderer/src/i18n/ru-RU/dialog.json | 45 +++ src/renderer/src/i18n/ru-RU/thread.json | 3 +- src/renderer/src/i18n/tr-TR/dialog.json | 45 +++ src/renderer/src/i18n/tr-TR/thread.json | 3 +- src/renderer/src/i18n/vi-VN/dialog.json | 45 +++ src/renderer/src/i18n/vi-VN/thread.json | 3 +- src/renderer/src/i18n/zh-CN/dialog.json | 45 +++ src/renderer/src/i18n/zh-CN/thread.json | 1 + src/renderer/src/i18n/zh-HK/dialog.json | 45 +++ src/renderer/src/i18n/zh-HK/thread.json | 3 +- src/renderer/src/i18n/zh-TW/dialog.json | 45 +++ src/renderer/src/i18n/zh-TW/thread.json | 3 +- src/renderer/src/stores/ui/session.ts | 16 + src/shared/contracts/routes.ts | 8 + .../contracts/routes/sessions.routes.ts | 45 +++ src/shared/types/agent-interface.d.ts | 45 ++- .../presenters/agent-session.presenter.d.ts | 10 +- .../agentSessionPresenter.test.ts | 294 +++++++++++++++ 60 files changed, 2469 insertions(+), 111 deletions(-) create mode 100644 src/renderer/src/components/agent/AgentTransferDialog.vue diff --git a/docs/features/agent-session-transfer/plan.md b/docs/features/agent-session-transfer/plan.md index dcbfe4526..b633008d1 100644 --- a/docs/features/agent-session-transfer/plan.md +++ b/docs/features/agent-session-transfer/plan.md @@ -13,7 +13,7 @@ `reassignAgentId(fromAgentId, toAgentId)`, but it does not expose a precise single-session ownership update or impact counts. - `AgentSessionPresenter` is the correct place to check runtime state, delete sessions with their - child subagents, emit session-list updates, and coordinate ACP workdir/session cleanup. + child subagents, emit session-list updates, and coordinate ACP session cleanup. - `AgentRuntimePresenter` caches session agent ids in memory, so ownership moves need a runtime hook, not just a database update. - `ChatTopBar.vue` already has a right-side `...` dropdown for writable sessions. Its current order @@ -30,13 +30,13 @@ Add typed route contracts in `src/shared/contracts/routes/sessions.routes.ts`: - input: `{ agentId: string }` - output: `{ impact: AgentTransferImpact }` - `sessions.moveAgentSessions` - - input: `{ fromAgentId: string; toAgentId: string; acpWorkdirBySessionId?: Record }` - - output: `{ movedSessionIds: string[]; deletedDraftSessionIds: string[]; skipped: AgentTransferSkip[] }` + - input: `{ fromAgentId: string; toAgentId: string }` + - output: `{ movedSessionIds: string[]; deletedSessionIds: string[] }` - `sessions.deleteAgentSessions` - input: `{ agentId: string }` - output: `{ deletedSessionIds: string[] }` - `sessions.moveSessionToAgent` - - input: `{ sessionId: string; toAgentId: string; acpWorkdir?: string }` + - input: `{ sessionId: string; toAgentId: string }` - output: `{ session: SessionWithState }` Proposed shared shape: @@ -57,7 +57,7 @@ type AgentTransferImpact = { isDraft: boolean projectDir: string | null status: 'idle' | 'generating' | 'error' - blockReason?: 'active' | 'pending-input' | 'missing-target-workdir' + blockReason?: 'active' | 'pending-input' }> } ``` @@ -86,12 +86,10 @@ Keep `config.listAgents` as the source for enabled target-agent options. - Resolve target runtime defaults: - DeepChat target: target agent config merged with built-in DeepChat defaults, then app default model fallback. - - ACP target: provider `acp`, model id target agent id, permission `full_access`, no DeepChat - disabled-tool list. + - ACP target: rejected. Conversation history must not move into ACP agents. - Update `new_sessions.agent_id` and any target-specific session fields in one logical operation. - Update the DeepChat runtime's session-agent cache and provider/model/generation settings. - Clear stale ACP binding for the previous ACP agent when moving away from ACP. - - For ACP targets, ensure/set workdir using the session project dir or the explicit `acpWorkdir`. - Emit `SESSION_EVENTS.LIST_UPDATED` / typed `sessions.updated`. - `deleteAgentSessions(agentId)` - Recompute impact. @@ -151,7 +149,7 @@ DropdownMenuContent The delete-agent dialog fetches: - impact via `SessionClient.getAgentTransferImpact(agentId)` -- target options via `ConfigClient.listAgents()`, filtered to enabled agents except source +- target options via `ConfigClient.listAgents()`, filtered to enabled DeepChat agents except source It submits: @@ -160,7 +158,7 @@ It submits: The chat-level dialog fetches the same target options and submits: -- move path: `SessionClient.moveSessionToAgent(sessionId, targetAgentId, acpWorkdir?)` +- move path: `SessionClient.moveSessionToAgent(sessionId, targetAgentId)` Dialog layout requirements: @@ -168,8 +166,8 @@ Dialog layout requirements: `max-h-[min(720px,calc(100vh-2rem))]` on desktop and `max-h-[calc(100vh-1rem)]` on narrow screens. - Keep the header and footer outside the scrolling region so the title and actions remain visible. -- Put impact summaries, affected chat samples, explanatory copy, target picker, and ACP workdir - controls in an internal `overflow-y-auto` body. +- Put impact summaries, affected chat samples, explanatory copy, and the target picker in an + internal `overflow-y-auto` body. - Prefer a single-column mobile layout; only use side-by-side summary/details areas on wider screens. - Ensure long agent names, session titles, and project paths truncate or wrap without widening the dialog. @@ -178,8 +176,8 @@ Dialog layout requirements: Chat-level move behavior: - Show only for active regular sessions. -- Disable while the session status is generating or while a required ACP workdir is missing. -- Use `SessionClient.moveSessionToAgent(sessionId, targetAgentId, acpWorkdir?)`. +- Disable while the session status is generating. +- Use `SessionClient.moveSessionToAgent(sessionId, targetAgentId)`. - Update `useSessionStore` with the returned session and let existing selected-agent sync run. ## Event Flow @@ -236,14 +234,16 @@ ChatTopBar right-side ... menu - empty drafts during delete-agent flow - related sessions only when the user explicitly picks the destructive option -## ACP Workdir Handling +## ACP Transfer Handling -- If moving to ACP and `session.projectDir` is non-empty, use it as the target ACP workdir. -- If moving a single chat to ACP and there is no project dir, ask for/select a workdir in the dialog. -- If batch-moving during agent deletion and some sessions lack project dirs, block with a clear list - and ask the user to either choose a shared workdir or delete those chats. -- Do not reuse the previous ACP `session_id`; call the existing ACP preparation path so the next turn - starts a fresh target-agent ACP session. +- ACP agents are allowed as sources only. A manual ACP agent's idle chats can move to an enabled + DeepChat agent before the ACP agent is deleted. +- ACP agents are never valid transfer targets. This blocks both DeepChat-to-ACP and ACP-to-ACP moves, + reducing the risk of future conflicts with ACP's external session bindings. +- When moving an ACP-backed chat to DeepChat, clear the stale ACP provider binding before applying + the target DeepChat runtime context. +- Because ACP is not a target, the first increment does not need a workdir picker in the transfer + dialog. ## Compatibility @@ -266,7 +266,7 @@ Main tests: - Impact summary counts regular/subagent/draft/blocked sessions. - Batch move DeepChat -> DeepChat applies target ownership and model defaults. - Batch move ACP -> DeepChat clears ACP binding and keeps messages. - - Move DeepChat -> ACP requires workdir and sets provider/model to ACP target. + - Moving to an ACP target is rejected for both DeepChat and ACP sources. - Active/generating sessions block move and delete. - `deleteAgentSessions` deletes related sessions through existing recursive cleanup. - `agentRuntimePresenter.test.ts` @@ -299,8 +299,8 @@ Quality gates after implementation: - Runtime cache drift is the main risk. Mitigation: transfer through AgentSessionPresenter and add a runtime-level context update method. -- ACP sessions may have provider-specific expectations about session continuity. Mitigation: only move - idle sessions and start a fresh target ACP binding. +- ACP sessions may have provider-specific expectations about session continuity. Mitigation: allow + ACP only as a source, clear stale ACP bindings when moving to DeepChat, and reject ACP targets. - Batch deletion is destructive. Mitigation: in-app dialog, explicit destructive option, and tests that verify move is the default/safe path. - Applying target DeepChat defaults can surprise users who expected current model settings to remain. diff --git a/docs/features/agent-session-transfer/spec.md b/docs/features/agent-session-transfer/spec.md index 2db06c5a1..aadd37e67 100644 --- a/docs/features/agent-session-transfer/spec.md +++ b/docs/features/agent-session-transfer/spec.md @@ -13,29 +13,31 @@ that agent's chats. The current implementation is inconsistent: the session list. The feature should turn agent deletion from an opaque destructive action into an explicit choice: -move/import related conversations to another agent, or delete those conversations together with the -agent. +move/import related conversations to another eligible DeepChat agent, or delete those conversations +together with the agent. ## User Need Users need to understand and control what happens to chats owned by an agent before deleting that -agent. They also need a normal chat-level way to move an idle conversation to another agent when the -conversation history is useful but the next turns should use a different agent. +agent. They also need a normal chat-level way to move an idle conversation to another DeepChat agent +when the conversation history is useful but the next turns should use a different agent. ## Goals - Show a structured delete-agent dialog whenever a deletable DeepChat or manual ACP agent has related sessions. - Offer two clear outcomes in that dialog: - - Move related sessions to another enabled agent, then delete the source agent. + - Move related sessions to another enabled DeepChat agent, then delete the source agent. - Delete related sessions, then delete the source agent. -- Allow regular idle sessions to move from one agent to another outside the delete flow. +- Allow regular idle sessions to move from their current agent to a DeepChat agent outside the delete + flow. - Expose exactly two first-increment migration entry points: - One-shot migration while deleting an agent. - A chat detail action from the active conversation's top-right `...` menu. -- Support DeepChat-to-DeepChat, DeepChat-to-ACP, ACP-to-DeepChat, and ACP-to-ACP moves for idle - sessions by preserving conversation history and reinitializing the target agent runtime for future - turns. +- Support DeepChat-to-DeepChat and ACP-to-DeepChat moves for idle sessions by preserving + conversation history and reinitializing the target DeepChat agent runtime for future turns. +- Prevent moves into ACP agents. DeepChat history must not be moved to ACP, and ACP-to-ACP moves are + blocked to avoid future ACP session binding conflicts. - Keep empty draft sessions from blocking deletion. ## Non-goals @@ -48,13 +50,13 @@ conversation history is useful but the next turns should use a different agent. deleted agent. - No deep copy/duplicate UI in the first increment. The first shipped action is move/import, not "duplicate and keep the original under the old agent". -- No attempt to preserve an ACP provider's external session id when moving to another agent. Target - ACP sessions start with a fresh ACP binding. +- No moving any conversation history into ACP agents in the first increment. ACP sessions may only + move out to a DeepChat agent. ## Terminology - **Move/import**: keep the same DeepChat session id and stored messages, change the owning - `agent_id`, and make future turns use the target agent. + `agent_id`, and make future turns use the target DeepChat agent. - **Related sessions**: rows in `new_sessions` whose `agent_id` is the agent being deleted, including regular sessions, subagent sessions, and drafts. - **Importable sessions**: related sessions that are idle and can be safely re-bound to a target @@ -66,12 +68,12 @@ conversation history is useful but the next turns should use a different agent. 1. As a user deleting a custom DeepChat agent, I can see how many chats will be affected before I confirm deletion. -2. As a user deleting a manual ACP agent, I can move that agent's finished chats to another agent so - they remain visible and usable. +2. As a user deleting a manual ACP agent, I can move that agent's finished chats to a DeepChat agent + so they remain visible and usable. 3. As a user who does not want to keep related chats, I can explicitly delete those chats together with the agent. -4. As a user viewing an idle regular conversation, I can move it to a different agent from the chat - UI, then continue the conversation with the target agent. +4. As a user viewing an idle regular conversation, I can move it to a different DeepChat agent from + the chat UI, then continue the conversation with the target agent. 5. As a user with an active conversation, I am told to stop or wait before moving the conversation. ## Acceptance Criteria @@ -80,16 +82,16 @@ conversation history is useful but the next turns should use a different agent. dialog rather than `window.confirm`. 2. The delete dialog shows counts for regular sessions, subagent sessions, empty drafts, and sessions that cannot currently be moved because they are active. -3. The primary safe action is "Move chats to..." and requires selecting an enabled target agent that - is not the source agent. +3. The primary safe action is "Move chats to..." and requires selecting an enabled DeepChat target + agent that is not the source agent. 4. The destructive action is "Delete chats and agent"; it clearly states that related chats will be removed. 5. If the source agent has no non-empty related sessions, deletion can use a shorter in-app confirm that states there are no chats to move. 6. Moving to a DeepChat agent applies the target agent's runtime defaults for future turns while preserving existing messages, attachments, search documents, tape entries, and title. -7. Moving to an ACP agent sets future turns to provider `acp` and model id equal to the target agent - id. If no project/workdir is available, the UI asks for one or blocks the move with a clear error. +7. Moving into ACP is not allowed. The UI must not list ACP agents as transfer targets, and the main + process must reject direct move requests whose target agent resolves to ACP. 8. Active or generating sessions cannot move. The dialog lists the blocked count and disables the move/delete completion until those sessions are stopped or finish. 9. After a successful delete-agent move, the source agent is removed and moved sessions appear under @@ -102,8 +104,9 @@ conversation history is useful but the next turns should use a different agent. actions visible, and scroll only the detailed body content when the impact list or help text is long. 13. All new user-facing text uses i18n keys. -14. Tests cover impact summary, DeepChat deletion with move, manual ACP deletion with move, explicit - delete of related sessions, chat-level move, and active-session blocking. +14. Tests cover impact summary, DeepChat deletion with move, manual ACP deletion with move to a + DeepChat target, explicit delete of related sessions, chat-level move, active-session blocking, + and rejection of ACP targets. ## UX States @@ -125,7 +128,7 @@ conversation history is useful but the next turns should use a different agent. | | | What should happen to these chats? | | | -| (o) Move chats to another Agent | +| (o) Move chats to another DeepChat Agent | | Target Agent | | [ DeepChat v ] | | | @@ -208,10 +211,9 @@ Move dialog | [ Code Reviewer v ] | | | | Existing messages and files stay in this conversation. | -| Future replies will use the target Agent. | -| | -| If the target is ACP and this chat has no project folder, | -| this area asks for a workdir. Long details scroll here. | +| Future replies will use the target DeepChat Agent. | +| ACP agents are not listed as targets. ACP chats can move | +| out to DeepChat, but chats cannot move into ACP. | +------------------------------------------------------------+ | [ Cancel ] [ Move ] | +------------------------------------------------------------+ @@ -225,7 +227,7 @@ Desktop / tablet | Fixed header: title, source agent/session | +------------------------------------------------------------+ | Scroll body: impact, affected chat samples, target picker, | -| explanatory copy, ACP workdir selector when needed | +| explanatory copy, target picker, affected chats | +------------------------------------------------------------+ | Fixed footer: cancel + primary/destructive action | +------------------------------------------------------------+ diff --git a/docs/features/agent-session-transfer/tasks.md b/docs/features/agent-session-transfer/tasks.md index c001784ca..46a6c5e84 100644 --- a/docs/features/agent-session-transfer/tasks.md +++ b/docs/features/agent-session-transfer/tasks.md @@ -1,26 +1,29 @@ # Tasks - Agent Session Transfer -- [ ] Contracts: add typed session transfer routes and shared schemas in `sessions.routes.ts`. -- [ ] Client: expose `getAgentTransferImpact`, `moveAgentSessions`, `deleteAgentSessions`, and +- [x] Contracts: add typed session transfer routes and shared schemas in `sessions.routes.ts`. +- [x] Client: expose `getAgentTransferImpact`, `moveAgentSessions`, `deleteAgentSessions`, and `moveSessionToAgent` from `SessionClient`. -- [ ] SQLite/session manager: add precise `updateAgentId(sessionId, agentId)` and any small counting +- [x] SQLite/session manager: add precise `updateAgentId(sessionId, agentId)` and any small counting helpers needed for impact summaries. -- [ ] Runtime: add a DeepChat runtime method to update session agent context without deleting +- [x] Runtime: add a DeepChat runtime method to update session agent context without deleting messages; reject generating sessions and invalidate agent-dependent caches. -- [ ] Main presenter: implement `AgentSessionPresenter` impact summary, batch move, single-session +- [x] Main presenter: implement `AgentSessionPresenter` impact summary, batch move, single-session move, and delete-by-agent flows. -- [ ] ACP handling: clear stale source ACP bindings and prepare/set target ACP workdir for ACP - targets. -- [ ] Agent deletion safety: remove silent DeepChat fallback reassignment and prevent deleting an +- [x] ACP handling: clear stale source ACP bindings when moving ACP chats to DeepChat, and block ACP + targets so DeepChat-to-ACP and ACP-to-ACP moves cannot occur. +- [x] Agent deletion safety: remove silent DeepChat fallback reassignment and prevent deleting an agent while sessions still point at it. -- [ ] Renderer dialog: build a responsive transfer dialog with a viewport-aware max height, fixed +- [x] Renderer dialog: build a responsive transfer dialog with a viewport-aware max height, fixed header/footer, internal scroll body, move/delete states, target-agent selection, blocked-session messaging, loading, and error states. -- [ ] Settings integration: replace `window.confirm` deletion in `DeepChatAgentsSettings.vue` and +- [x] Settings integration: replace `window.confirm` deletion in `DeepChatAgentsSettings.vue` and manual-agent deletion in `AcpSettings.vue`. -- [ ] Chat-level move: add `Move conversation` to `ChatTopBar.vue`'s right-side `...` menu between +- [x] Chat-level move: add `Move conversation` to `ChatTopBar.vue`'s right-side `...` menu between pin/unpin and clear messages, then wire it to the transfer dialog and store/client integration. -- [ ] i18n: add English and Chinese strings first, then run the repository i18n workflow for other +- [x] i18n: add English and Chinese strings first, then run the repository i18n workflow for other locales. -- [ ] Tests: add main presenter/runtime/repository coverage and renderer dialog/store coverage. -- [ ] Validation: run `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, and targeted tests. +- [ ] Tests: main presenter coverage exists for impact summaries, DeepChat moves, ACP-to-DeepChat, + and ACP target rejection; renderer dialog/store and repository regression tests remain as + follow-up coverage. +- [x] Validation: run `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, typecheck, targeted + tests, and `git diff --check`. diff --git a/src/main/presenter/agentRepository/index.ts b/src/main/presenter/agentRepository/index.ts index cc2d2368a..e63f3f0d3 100644 --- a/src/main/presenter/agentRepository/index.ts +++ b/src/main/presenter/agentRepository/index.ts @@ -192,7 +192,14 @@ export class AgentRepository { return false } - this.sqlitePresenter.newSessionsTable.reassignAgentId(agentId, BUILTIN_DEEPCHAT_AGENT_ID) + const relatedSessions = this.sqlitePresenter.newSessionsTable.list({ + agentId, + includeSubagents: true + }) + if (relatedSessions.length > 0) { + return false + } + this.sqlitePresenter.agentsTable.delete(agentId) return true } @@ -287,6 +294,13 @@ export class AgentRepository { if (!row || row.agent_type !== 'acp' || row.source !== 'manual') { return false } + const relatedSessions = this.sqlitePresenter.newSessionsTable.list({ + agentId, + includeSubagents: true + }) + if (relatedSessions.length > 0) { + return false + } this.sqlitePresenter.agentsTable.delete(agentId) return true } diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index 3d1731106..6b43db139 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -20,6 +20,7 @@ import type { QueuePendingInputOptions, SendMessageInput, SessionCompactionState, + SessionAgentContextUpdate, SessionGenerationSettings, ToolInteractionResponse, ToolInteractionResult, @@ -1424,6 +1425,54 @@ export class AgentRuntimePresenter implements IAgentImplementation { this.invalidateToolProfileCache(sessionId) } + async setSessionAgentContext( + sessionId: string, + config: SessionAgentContextUpdate + ): Promise { + const nextProviderId = config.providerId?.trim() + const nextModelId = config.modelId?.trim() + const nextAgentId = config.agentId?.trim() + if (!nextAgentId || !nextProviderId || !nextModelId) { + throw new Error('Session agent context update requires agentId, providerId and modelId.') + } + + const state = this.runtimeState.get(sessionId) + const dbSession = this.sessionStore.get(sessionId) + if (!state && !dbSession) { + throw new Error(`Session ${sessionId} not found`) + } + + if (state?.status === 'generating') { + throw new Error('Cannot move session while it is generating.') + } + + const permissionMode: PermissionMode = + config.permissionMode === 'default' ? 'default' : 'full_access' + const sanitizedGenerationSettings = await this.sanitizeGenerationSettings( + nextProviderId, + nextModelId, + config.generationSettings ?? {} + ) + + this.runtimeState.set(sessionId, { + status: state?.status ?? 'idle', + providerId: nextProviderId, + modelId: nextModelId, + permissionMode + }) + this.sessionStore.updateSessionModel(sessionId, nextProviderId, nextModelId) + this.sessionStore.updatePermissionMode(sessionId, permissionMode) + this.sessionStore.updateGenerationSettings( + sessionId, + this.buildPersistedGenerationSettingsReplacement(sanitizedGenerationSettings) + ) + this.sessionAgentIds.set(sessionId, nextAgentId) + this.sessionProjectDirs.set(sessionId, this.normalizeProjectDir(config.projectDir)) + this.sessionGenerationSettings.set(sessionId, sanitizedGenerationSettings) + this.invalidateSystemPromptCache(sessionId) + this.invalidateToolProfileCache(sessionId) + } + async setSessionProjectDir(sessionId: string, projectDir: string | null): Promise { const normalized = this.normalizeProjectDir(projectDir) const previous = this.sessionProjectDirs.has(sessionId) diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts index 65cff750b..595195aff 100644 --- a/src/main/presenter/agentSessionPresenter/index.ts +++ b/src/main/presenter/agentSessionPresenter/index.ts @@ -5,6 +5,9 @@ import type { AgentTapeInfo, AgentTapeSearchOptions, AgentTapeSearchResult, + AgentTransferBlockReason, + AgentTransferImpact, + AgentTransferImpactSample, ChatMessagePageResult, SessionListItem, SessionLightweightListResult, @@ -92,6 +95,18 @@ type SearchableMessageRow = { updatedAt: number } +type AgentTransferTargetContext = { + agentId: string + agentType: 'deepchat' + providerId: string + modelId: string + projectDir: string | null + permissionMode: PermissionMode + generationSettings?: Partial + disabledAgentTools: string[] + subagentEnabled: boolean +} + const SUBAGENT_SESSION_INIT_MAX_ATTEMPTS = 2 const SQLITE_MAINLINE_NORMALIZATION_KEY = 'sqlite-mainline-normalization-v1' @@ -1860,6 +1875,166 @@ export class AgentSessionPresenter { }) } + async getAgentTransferImpact(agentId: string): Promise { + const sessions = this.sessionManager.list({ + agentId, + includeSubagents: true + }) + const samples: AgentTransferImpactSample[] = [] + let emptyDrafts = 0 + let movableSessions = 0 + let blockedSessions = 0 + + for (const session of sessions) { + const assessment = await this.assessTransferSession(session) + if (assessment.isEmptyDraft) { + emptyDrafts += 1 + } + if (assessment.blockReason) { + blockedSessions += 1 + } else if (!assessment.isEmptyDraft) { + movableSessions += 1 + } + + if (samples.length < 6 && (!assessment.isEmptyDraft || assessment.blockReason)) { + samples.push({ + id: session.id, + title: session.title, + sessionKind: session.sessionKind, + isDraft: Boolean(session.isDraft), + projectDir: session.projectDir, + status: assessment.status, + blockReason: assessment.blockReason + }) + } + } + + return { + agentId, + totalSessions: sessions.length, + regularSessions: sessions.filter((session) => session.sessionKind === 'regular').length, + subagentSessions: sessions.filter((session) => session.sessionKind === 'subagent').length, + emptyDrafts, + movableSessions, + blockedSessions, + samples + } + } + + async moveAgentSessions( + fromAgentId: string, + toAgentId: string + ): Promise<{ movedSessionIds: string[]; deletedSessionIds: string[] }> { + const sourceAgentId = fromAgentId.trim() + const targetAgentId = toAgentId.trim() + if (!sourceAgentId || !targetAgentId) { + throw new Error('Source and target agent ids are required.') + } + if (sourceAgentId === targetAgentId) { + throw new Error('Source and target agents cannot be the same.') + } + await this.resolveTransferTargetContext(targetAgentId, null) + + const sessions = this.sessionManager.list({ + agentId: sourceAgentId, + includeSubagents: true + }) + const transferSessionIds: string[] = [] + const emptyDraftSessionIds: string[] = [] + const movedSessionIds: string[] = [] + const deletedSessionIds: string[] = [] + const deletedSessionIdSet = new Set() + + for (const session of sessions) { + const assessment = await this.assessTransferSession(session) + if (assessment.blockReason) { + throw new Error(`Session ${session.id} cannot be moved: ${assessment.blockReason}`) + } + if (assessment.isEmptyDraft) { + emptyDraftSessionIds.push(session.id) + continue + } + + await this.resolveTransferTargetContext(targetAgentId, session.projectDir) + transferSessionIds.push(session.id) + } + + for (const sessionId of transferSessionIds) { + if (deletedSessionIdSet.has(sessionId) || !this.sessionManager.get(sessionId)) { + continue + } + await this.moveSessionToAgentInternal(sessionId, targetAgentId, true) + movedSessionIds.push(sessionId) + } + + for (const sessionId of emptyDraftSessionIds) { + if (deletedSessionIdSet.has(sessionId) || !this.sessionManager.get(sessionId)) { + continue + } + const deleted = await this.deleteSessionInternal(sessionId) + deleted.forEach((deletedSessionId) => deletedSessionIdSet.add(deletedSessionId)) + deletedSessionIds.push(...deleted) + } + + if (movedSessionIds.length > 0) { + this.emitSessionListUpdated({ + sessionIds: movedSessionIds, + reason: 'updated' + }) + } + if (deletedSessionIds.length > 0) { + this.emitSessionListUpdated({ + sessionIds: deletedSessionIds, + reason: 'deleted' + }) + } + + return { movedSessionIds, deletedSessionIds } + } + + async deleteAgentSessions(agentId: string): Promise { + const sessions = this.sessionManager.list({ + agentId: agentId.trim(), + includeSubagents: true + }) + const deletedSessionIds: string[] = [] + const deletedSessionIdSet = new Set() + + for (const session of sessions) { + const assessment = await this.assessTransferSession(session) + if (assessment.blockReason) { + throw new Error(`Session ${session.id} cannot be deleted: ${assessment.blockReason}`) + } + } + + for (const session of sessions) { + if (deletedSessionIdSet.has(session.id) || !this.sessionManager.get(session.id)) { + continue + } + const deleted = await this.deleteSessionInternal(session.id) + deleted.forEach((sessionId) => deletedSessionIdSet.add(sessionId)) + deletedSessionIds.push(...deleted) + } + + if (deletedSessionIds.length > 0) { + this.emitSessionListUpdated({ + sessionIds: deletedSessionIds, + reason: 'deleted' + }) + } + + return deletedSessionIds + } + + async moveSessionToAgent(sessionId: string, toAgentId: string): Promise { + const updated = await this.moveSessionToAgentInternal(sessionId, toAgentId) + this.emitSessionListUpdated({ + sessionIds: [sessionId], + reason: 'updated' + }) + return updated + } + async cancelGeneration(sessionId: string): Promise { const session = this.sessionManager.get(sessionId) if (!session) return @@ -2069,7 +2244,11 @@ export class AgentSessionPresenter { sessionIds: [sessionId], reason: 'updated' }) - return await this.tryBuildSessionWithState(updated) + const sessionWithState = await this.tryBuildSessionWithState(updated) + if (!sessionWithState) { + throw new Error(`Failed to build session state after project update: ${sessionId}`) + } + return sessionWithState } async getSessionGenerationSettings(sessionId: string): Promise { @@ -2484,6 +2663,163 @@ export class AgentSessionPresenter { } } + private async assessTransferSession(session: SessionRecord): Promise<{ + status: SessionWithState['status'] + isEmptyDraft: boolean + blockReason?: AgentTransferBlockReason + }> { + const agent = await this.resolveAgentImplementation(session.agentId) + const state = await agent.getSessionState(session.id) + const status = state?.status ?? 'idle' + const hasMessages = await this.hasSessionMessages(agent, session.id) + const hasSubagentChildren = + session.sessionKind === 'regular' && + this.sessionManager.list({ includeSubagents: true, parentSessionId: session.id }).length > 0 + const isEmptyDraft = Boolean(session.isDraft) && !hasMessages && !hasSubagentChildren + let hasPendingInput = false + + if (agent.listPendingInputs) { + try { + hasPendingInput = (await agent.listPendingInputs(session.id)).length > 0 + } catch (error) { + console.warn( + `[AgentSessionPresenter] Failed to inspect pending input for session=${session.id}:`, + error + ) + hasPendingInput = true + } + } + + if (status === 'generating') { + return { status, isEmptyDraft, blockReason: 'active' } + } + if (hasPendingInput) { + return { status, isEmptyDraft, blockReason: 'pending-input' } + } + + return { status, isEmptyDraft } + } + + private async moveSessionToAgentInternal( + sessionId: string, + toAgentId: string, + allowSubagent: boolean = false + ): Promise { + const session = this.sessionManager.get(sessionId) + if (!session) { + throw new Error(`Session not found: ${sessionId}`) + } + if (!allowSubagent && session.sessionKind !== 'regular') { + throw new Error('Only regular conversations can be moved from the conversation menu.') + } + + const targetAgentId = toAgentId.trim() + if (!targetAgentId) { + throw new Error('Target agent id is required.') + } + if (session.agentId === targetAgentId) { + throw new Error('Conversation is already assigned to the selected agent.') + } + + const assessment = await this.assessTransferSession(session) + if (assessment.blockReason) { + throw new Error(`Session ${sessionId} cannot be moved: ${assessment.blockReason}`) + } + + const previousAgentId = session.agentId + const targetContext = await this.resolveTransferTargetContext(targetAgentId, session.projectDir) + const previousAcpBacked = await this.isAcpBackedSession(sessionId, previousAgentId) + const agent = await this.resolveAgentImplementation(targetContext.agentId) + + if (!agent.setSessionAgentContext) { + throw new Error(`Agent ${targetContext.agentId} does not support session transfer.`) + } + + if (previousAcpBacked) { + await (this.providerSessionPort?.clearAcpSession?.(sessionId) ?? + this.llmProviderPresenter.clearAcpSession(sessionId)) + } + + await agent.setSessionAgentContext(sessionId, { + agentId: targetContext.agentId, + providerId: targetContext.providerId, + modelId: targetContext.modelId, + projectDir: targetContext.projectDir, + permissionMode: targetContext.permissionMode, + generationSettings: targetContext.generationSettings + }) + + this.sessionManager.updateAgentId(sessionId, targetContext.agentId) + this.sessionManager.update(sessionId, { + projectDir: targetContext.projectDir, + subagentEnabled: session.sessionKind === 'regular' ? targetContext.subagentEnabled : false + }) + this.sessionManager.updateDisabledAgentTools(sessionId, targetContext.disabledAgentTools) + + await this.syncAcpSessionWorkdir( + targetContext.providerId, + sessionId, + targetContext.agentId, + targetContext.projectDir + ) + + const updated = this.sessionManager.get(sessionId) + if (!updated) { + throw new Error(`Session not found after transfer: ${sessionId}`) + } + + const sessionWithState = await this.tryBuildSessionWithState(updated) + if (!sessionWithState) { + throw new Error(`Failed to build session state after transfer: ${sessionId}`) + } + return sessionWithState + } + + private async resolveTransferTargetContext( + targetAgentId: string, + currentProjectDir: string | null + ): Promise { + const resolvedAgentId = resolveAcpAgentAlias(targetAgentId.trim()) + const agentType = await this.getAgentType(resolvedAgentId) + if (agentType === 'acp') { + throw new Error('Conversation history cannot be moved to ACP agents.') + } + if (agentType !== 'deepchat') { + throw new Error(`Target agent not found: ${targetAgentId}`) + } + + const currentProject = currentProjectDir?.trim() || null + const config = await this.resolveDeepChatAgentConfigCompat(resolvedAgentId) + const defaultModel = this.configPresenter.getDefaultModel() + const providerId = + config?.defaultModelPreset?.providerId?.trim() || defaultModel?.providerId?.trim() || '' + const modelId = + config?.defaultModelPreset?.modelId?.trim() || defaultModel?.modelId?.trim() || '' + if (!providerId || !modelId) { + throw new Error('Target DeepChat agent does not have a default model.') + } + + return { + agentId: resolvedAgentId, + agentType, + providerId, + modelId, + projectDir: + currentProject || + config?.defaultProjectPath?.trim() || + this.getDefaultProjectPathCompat() || + null, + permissionMode: config?.permissionMode === 'default' ? 'default' : 'full_access', + generationSettings: this.mergeDeepChatDefaultGenerationSettings(config), + disabledAgentTools: this.normalizeDisabledAgentTools(config?.disabledAgentTools), + subagentEnabled: this.resolveSessionSubagentEnabled( + agentType, + undefined, + config?.subagentEnabled + ) + } + } + private async deleteSessionInternal(sessionId: string): Promise { const session = this.sessionManager.get(sessionId) if (!session) return [] diff --git a/src/main/presenter/agentSessionPresenter/sessionManager.ts b/src/main/presenter/agentSessionPresenter/sessionManager.ts index 4779b6532..6357e63b7 100644 --- a/src/main/presenter/agentSessionPresenter/sessionManager.ts +++ b/src/main/presenter/agentSessionPresenter/sessionManager.ts @@ -203,6 +203,16 @@ export class NewSessionManager { this.sqlitePresenter.newEnvironmentsTable.syncForSession(id) } + updateAgentId(id: string, agentId: string): void { + const current = this.sqlitePresenter.newSessionsTable.get(id) + if (!current || current.agent_id === agentId) { + return + } + + this.sqlitePresenter.newSessionsTable.updateAgentId(id, agentId) + this.sqlitePresenter.newEnvironmentsTable.syncForSession(id) + } + // Window binding management bindWindow(webContentsId: number, sessionId: string): void { this.windowBindings.set(webContentsId, sessionId) diff --git a/src/main/presenter/sqlitePresenter/tables/newSessions.ts b/src/main/presenter/sqlitePresenter/tables/newSessions.ts index 939f17f85..2f977955f 100644 --- a/src/main/presenter/sqlitePresenter/tables/newSessions.ts +++ b/src/main/presenter/sqlitePresenter/tables/newSessions.ts @@ -398,6 +398,12 @@ export class NewSessionsTable extends BaseTable { this.update(id, { disabled_agent_tools: JSON.stringify(disabledAgentTools) }) } + updateAgentId(id: string, agentId: string): void { + this.db + .prepare('UPDATE new_sessions SET agent_id = ?, updated_at = ? WHERE id = ?') + .run(agentId, Date.now(), id) + } + reassignAgentId(fromAgentId: string, toAgentId: string): void { this.db .prepare('UPDATE new_sessions SET agent_id = ?, updated_at = ? WHERE agent_id = ?') diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index fc0e7ddf1..77a1cca46 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -123,6 +123,7 @@ import { sessionsCompactRoute, sessionsConvertPendingInputToSteerRoute, sessionsCreateRoute, + sessionsDeleteAgentSessionsRoute, sessionsDeleteMessageRoute, sessionsDeletePendingInputRoute, sessionsDeleteRoute, @@ -135,6 +136,7 @@ import { sessionsGetAcpSessionConfigOptionsRoute, sessionsGetActiveRoute, sessionsGetAgentsRoute, + sessionsGetAgentTransferImpactRoute, sessionsGetDisabledAgentToolsRoute, sessionsGetLightweightByIdsRoute, sessionsGetGenerationSettingsRoute, @@ -145,7 +147,9 @@ import { sessionsListRoute, sessionsListMessageTracesRoute, sessionsListPendingInputsRoute, + sessionsMoveAgentSessionsRoute, sessionsMoveQueuedInputRoute, + sessionsMoveToAgentRoute, sessionsQueuePendingInputRoute, sessionsRenameRoute, sessionsResumePendingQueueRoute, @@ -1952,6 +1956,38 @@ export async function dispatchDeepchatRoute( return sessionsDeleteRoute.output.parse({ deleted: true }) } + case sessionsGetAgentTransferImpactRoute.name: { + const input = sessionsGetAgentTransferImpactRoute.input.parse(rawInput) + const impact = await runtime.agentSessionPresenter.getAgentTransferImpact(input.agentId) + return sessionsGetAgentTransferImpactRoute.output.parse({ impact }) + } + + case sessionsMoveAgentSessionsRoute.name: { + const input = sessionsMoveAgentSessionsRoute.input.parse(rawInput) + const result = await runtime.agentSessionPresenter.moveAgentSessions( + input.fromAgentId, + input.toAgentId + ) + return sessionsMoveAgentSessionsRoute.output.parse(result) + } + + case sessionsDeleteAgentSessionsRoute.name: { + const input = sessionsDeleteAgentSessionsRoute.input.parse(rawInput) + const deletedSessionIds = await runtime.agentSessionPresenter.deleteAgentSessions( + input.agentId + ) + return sessionsDeleteAgentSessionsRoute.output.parse({ deletedSessionIds }) + } + + case sessionsMoveToAgentRoute.name: { + const input = sessionsMoveToAgentRoute.input.parse(rawInput) + const session = await runtime.agentSessionPresenter.moveSessionToAgent( + input.sessionId, + input.toAgentId + ) + return sessionsMoveToAgentRoute.output.parse({ session }) + } + case sessionsGetAcpSessionCommandsRoute.name: { const input = sessionsGetAcpSessionCommandsRoute.input.parse(rawInput) const commands = await runtime.agentSessionPresenter.getAcpSessionCommands(input.sessionId) diff --git a/src/renderer/api/SessionClient.ts b/src/renderer/api/SessionClient.ts index f5a869320..bc1e32632 100644 --- a/src/renderer/api/SessionClient.ts +++ b/src/renderer/api/SessionClient.ts @@ -13,6 +13,7 @@ import { sessionsCompactRoute, sessionsConvertPendingInputToSteerRoute, sessionsCreateRoute, + sessionsDeleteAgentSessionsRoute, sessionsDeleteMessageRoute, sessionsDeletePendingInputRoute, sessionsDeleteRoute, @@ -25,6 +26,7 @@ import { sessionsGetAcpSessionConfigOptionsRoute, sessionsGetActiveRoute, sessionsGetAgentsRoute, + sessionsGetAgentTransferImpactRoute, sessionsGetDisabledAgentToolsRoute, sessionsGetLightweightByIdsRoute, sessionsGetGenerationSettingsRoute, @@ -35,7 +37,9 @@ import { sessionsListRoute, sessionsListMessageTracesRoute, sessionsListPendingInputsRoute, + sessionsMoveAgentSessionsRoute, sessionsMoveQueuedInputRoute, + sessionsMoveToAgentRoute, sessionsQueuePendingInputRoute, sessionsRenameRoute, sessionsResumePendingQueueRoute, @@ -274,6 +278,31 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() await bridge.invoke(sessionsDeleteRoute.name, { sessionId }) } + async function getAgentTransferImpact(agentId: string) { + const result = await bridge.invoke(sessionsGetAgentTransferImpactRoute.name, { agentId }) + return result.impact + } + + async function moveAgentSessions(fromAgentId: string, toAgentId: string) { + return await bridge.invoke(sessionsMoveAgentSessionsRoute.name, { + fromAgentId, + toAgentId + }) + } + + async function deleteAgentSessions(agentId: string) { + const result = await bridge.invoke(sessionsDeleteAgentSessionsRoute.name, { agentId }) + return result.deletedSessionIds + } + + async function moveSessionToAgent(sessionId: string, toAgentId: string) { + const result = await bridge.invoke(sessionsMoveToAgentRoute.name, { + sessionId, + toAgentId + }) + return result.session + } + async function getAcpSessionCommands(sessionId: string) { const result = await bridge.invoke(sessionsGetAcpSessionCommandsRoute.name, { sessionId }) return result.commands @@ -466,6 +495,10 @@ export function createSessionClient(bridge: DeepchatBridge = getDeepchatBridge() compactSession, exportSession, deleteSession, + getAgentTransferImpact, + moveAgentSessions, + deleteAgentSessions, + moveSessionToAgent, getAcpSessionCommands, getAcpSessionConfigOptions, setAcpSessionConfigOption, diff --git a/src/renderer/settings/components/AcpSettings.vue b/src/renderer/settings/components/AcpSettings.vue index 401a532d9..60a8c9b83 100644 --- a/src/renderer/settings/components/AcpSettings.vue +++ b/src/renderer/settings/components/AcpSettings.vue @@ -279,7 +279,12 @@ -