-
+
{/* Scrollbar is the scroll viewport; the cmdk list itself must not scroll so keyboard
navigation's scroll-into-view bubbles up to the styled Scrollbar instead. */}
{/* Pinned at the top, but hidden while searching so the query's first match keeps the
default keyboard highlight instead of this row. */}
{createAction && !query.trim() ? (
-
+ createAction.onSelect()}>
{createAction.icon}
@@ -155,12 +155,12 @@ export function ConversationPickerDialog({
{labels.loadingText}
{topicName && (
<>
diff --git a/src/renderer/pages/home/components/__tests__/TopicRightPane.test.tsx b/src/renderer/pages/home/components/__tests__/TopicRightPane.test.tsx
index 7977e28e91a..85f06952d62 100644
--- a/src/renderer/pages/home/components/__tests__/TopicRightPane.test.tsx
+++ b/src/renderer/pages/home/components/__tests__/TopicRightPane.test.tsx
@@ -36,9 +36,9 @@ vi.mock('@renderer/components/chat/shell/RightPaneHost', async () => {
return {
ARTIFACT_RIGHT_PANE_CACHE_KEY: 'ui.chat.artifact_pane.width',
- ARTIFACT_RIGHT_PANE_DEFAULT_WIDTH: 460,
+ ARTIFACT_RIGHT_PANE_DEFAULT_WIDTH: 280,
ARTIFACT_RIGHT_PANE_MAX_WIDTH: 720,
- ARTIFACT_RIGHT_PANE_MIN_WIDTH: 360,
+ ARTIFACT_RIGHT_PANE_MIN_WIDTH: 280,
RightPaneHost: ({
children,
onCloseAnimationComplete,
diff --git a/src/renderer/pages/launchpad/__tests__/LaunchpadPage.test.tsx b/src/renderer/pages/launchpad/__tests__/LaunchpadPage.test.tsx
index d042c376cb2..b6aad76160d 100644
--- a/src/renderer/pages/launchpad/__tests__/LaunchpadPage.test.tsx
+++ b/src/renderer/pages/launchpad/__tests__/LaunchpadPage.test.tsx
@@ -122,7 +122,7 @@ vi.mock('react-i18next', () => ({
const label =
{
'agent.sidebar_title': 'Agent',
- 'agent.session.group.conversation': 'Chat',
+ 'title.chat': 'Chat',
'assistants.presets.title': 'Library',
'code.title': 'Code',
'files.title': 'Files',
diff --git a/src/renderer/pages/settings/AppearanceSettings/AppearanceSettings.tsx b/src/renderer/pages/settings/AppearanceSettings/AppearanceSettings.tsx
index 7bc8e1f98fd..afee2f978bc 100644
--- a/src/renderer/pages/settings/AppearanceSettings/AppearanceSettings.tsx
+++ b/src/renderer/pages/settings/AppearanceSettings/AppearanceSettings.tsx
@@ -33,7 +33,7 @@ import i18n from '@renderer/i18n/resolver'
import { formatErrorMessage } from '@renderer/utils/error'
import { isLinux, isMac } from '@renderer/utils/platform'
import { cn } from '@renderer/utils/style'
-import type { ChatLayoutMode, LanguageVarious, MenuPresentationMode } from '@shared/data/preference/preferenceTypes'
+import type { LanguageVarious, MenuPresentationMode } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { defaultLanguage } from '@shared/utils/languages'
import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react'
@@ -128,8 +128,6 @@ const AppearanceSettings: FC = () => {
const [menuPresentationMode, setMenuPresentationMode] = usePreference('menu.presentation_mode')
const [customCss, setCustomCss] = usePreference('ui.custom_css')
const [fontSize] = usePreference('chat.message.font_size')
- const [topicLayout, setTopicLayout] = usePreference('topic.layout')
- const [sessionLayout, setSessionLayout] = usePreference('agent.layout')
const [useSystemTitleBar, setUseSystemTitleBar] = usePreference('app.use_system_title_bar')
const [codeExecution, setCodeExecution] = useMultiplePreferences({
enabled: 'chat.code.execution.enabled',
@@ -240,14 +238,6 @@ const AppearanceSettings: FC = () => {
[t]
)
- const layoutOptions = useMemo(
- () => [
- { value: 'classic' as const, label: t('settings.messages.layout.classic') },
- { value: 'modern' as const, label: t('settings.messages.layout.modern') }
- ],
- [t]
- )
-
const handleMenuPresentationModeChange = useCallback(
(mode: MenuPresentationMode) => {
confirmMenuPresentationModeChange({
@@ -464,30 +454,6 @@ const AppearanceSettings: FC = () => {
)}
-
- {t('settings.display.topic.title')}
-
-
- {t('settings.messages.layout.conversation')}
-
- value={topicLayout}
- onValueChange={setTopicLayout}
- options={layoutOptions}
- size="sm"
- />
-
-
-
- {t('settings.messages.layout.work')}
-
- value={sessionLayout}
- onValueChange={setSessionLayout}
- options={layoutOptions}
- size="sm"
- />
-
-
-
{t('settings.display.font.title')} New
diff --git a/src/renderer/pages/settings/AppearanceSettings/__tests__/AppearanceSettings.test.tsx b/src/renderer/pages/settings/AppearanceSettings/__tests__/AppearanceSettings.test.tsx
index decd007414b..adb3358e412 100644
--- a/src/renderer/pages/settings/AppearanceSettings/__tests__/AppearanceSettings.test.tsx
+++ b/src/renderer/pages/settings/AppearanceSettings/__tests__/AppearanceSettings.test.tsx
@@ -317,4 +317,16 @@ describe('AppearanceSettings language selector', () => {
expect(screen.getByRole('combobox', { name: /中文/ })).toBeInTheDocument()
expect(screen.queryByRole('combobox', { name: /English/ })).not.toBeInTheDocument()
})
+
+ it('does not render manual chat layout switches', async () => {
+ render()
+
+ await waitFor(() => {
+ expect(window.api.getSystemFonts).toHaveBeenCalled()
+ expect(window.api.handleZoomFactor).toHaveBeenCalled()
+ })
+
+ expect(screen.queryByText('settings.messages.layout.conversation')).not.toBeInTheDocument()
+ expect(screen.queryByText('settings.messages.layout.work')).not.toBeInTheDocument()
+ })
})
diff --git a/src/renderer/utils/__tests__/resourceEntity.test.ts b/src/renderer/utils/__tests__/resourceEntity.test.ts
index 227e4c5059b..3a098bedc64 100644
--- a/src/renderer/utils/__tests__/resourceEntity.test.ts
+++ b/src/renderer/utils/__tests__/resourceEntity.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
-import { findLatestUpdated, isUntouchedSinceCreation } from '../resourceEntity'
+import { findLatestUpdated, isUntouchedSinceCreation, pickNeighbourAfterRemoval } from '../resourceEntity'
describe('resourceEntity', () => {
describe('isUntouchedSinceCreation', () => {
@@ -66,4 +66,23 @@ describe('resourceEntity', () => {
expect(findLatestUpdated([first, second])).toBe(first)
})
})
+
+ describe('pickNeighbourAfterRemoval', () => {
+ const list = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
+
+ it('picks the next row in display order', () => {
+ expect(pickNeighbourAfterRemoval(list, 'a')).toEqual({ id: 'b' })
+ expect(pickNeighbourAfterRemoval(list, 'b')).toEqual({ id: 'c' })
+ })
+
+ it('picks the previous row when the removed row was last', () => {
+ expect(pickNeighbourAfterRemoval(list, 'c')).toEqual({ id: 'b' })
+ })
+
+ it('returns undefined when the id is absent or it was the only row', () => {
+ expect(pickNeighbourAfterRemoval(list, 'missing')).toBeUndefined()
+ expect(pickNeighbourAfterRemoval([{ id: 'only' }], 'only')).toBeUndefined()
+ expect(pickNeighbourAfterRemoval([], 'a')).toBeUndefined()
+ })
+ })
})
diff --git a/src/renderer/utils/resourceEntity.ts b/src/renderer/utils/resourceEntity.ts
index 98d0007290c..e274c019e2c 100644
--- a/src/renderer/utils/resourceEntity.ts
+++ b/src/renderer/utils/resourceEntity.ts
@@ -28,6 +28,26 @@ export function isUntouchedSinceCreation(item: { createdAt?: string; updatedAt?:
return Math.abs(updatedAtMs - createdAtMs) <= UNTOUCHED_SINCE_CREATION_TOLERANCE_MS
}
+/**
+ * Selection policy for "which row becomes active after the active one is deleted": pick the next
+ * row in the given display-ordered list, or the previous row when the deleted row was last. Returns
+ * `undefined` when `id` is not present or was the only row (callers decide the empty fallback).
+ *
+ * `orderedList` must be the list in *visible display order* (and scoped to the surface the deleted
+ * row lived in), and must still contain the deleted row — call this on the pre-refresh snapshot.
+ * Centralizes the topic and agent-session delete-selection so both surfaces stay consistent instead
+ * of one picking the display neighbour and the other the raw API/orderKey head.
+ */
+export function pickNeighbourAfterRemoval(
+ orderedList: readonly T[],
+ id: string
+): T | undefined {
+ if (orderedList.length <= 1) return undefined
+ const index = orderedList.findIndex((item) => item.id === id)
+ if (index === -1) return undefined
+ return orderedList[index + 1 === orderedList.length ? index - 1 : index + 1]
+}
+
/**
* Return the entity with the most recent `updatedAt` (ISO string). Ties keep the first item;
* a missing or unparseable `updatedAt` sorts as oldest. Returns `undefined` for an empty list.
diff --git a/src/shared/data/cache/cacheSchemas.ts b/src/shared/data/cache/cacheSchemas.ts
index 928b04a4f5c..5b463db4a11 100644
--- a/src/shared/data/cache/cacheSchemas.ts
+++ b/src/shared/data/cache/cacheSchemas.ts
@@ -313,8 +313,9 @@ export type RendererPersistCacheSchema = {
// Sidebar section/group collapse — one fixed key per display mode so toggling a group in one
// mode never re-writes the others (avoids the whole-blob cross-mode/cross-window clobber).
// Stores the flat list of collapsed section/group ids; empty = everything expanded.
+ // Null means no user preference has been written yet, so the view may apply its default.
'ui.topic.expansion.time': string[]
- 'ui.topic.expansion.assistant': string[]
+ 'ui.topic.expansion.assistant': string[] | null
'ui.agent.last_used_session_id': string | null
'ui.agent.last_used_agent_id': string | null
'ui.agent.last_used_workspace_id': string | null
@@ -322,8 +323,8 @@ export type RendererPersistCacheSchema = {
// 'ui.chat.right_pane_open'); kept separate so the assistant and agent surfaces don't bleed.
'ui.agent.right_pane_open': boolean
'ui.agent.session.expansion.time': string[]
- 'ui.agent.session.expansion.agent': string[]
- 'ui.agent.session.expansion.workdir': string[]
+ 'ui.agent.session.expansion.agent': string[] | null
+ 'ui.agent.session.expansion.workdir': string[] | null
'settings.provider.last_selected_provider_id': string | null
'settings.provider.openai.alert.dismissed': boolean
'feature.mcp.is_uv_installed': boolean
@@ -345,14 +346,14 @@ export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
'ui.chat.last_used_topic_id': null,
'ui.chat.right_pane_open': false,
'ui.topic.expansion.time': [],
- 'ui.topic.expansion.assistant': [],
+ 'ui.topic.expansion.assistant': null,
'ui.agent.last_used_session_id': null,
'ui.agent.last_used_agent_id': null,
'ui.agent.last_used_workspace_id': null,
'ui.agent.right_pane_open': false,
'ui.agent.session.expansion.time': [],
- 'ui.agent.session.expansion.agent': [],
- 'ui.agent.session.expansion.workdir': [],
+ 'ui.agent.session.expansion.agent': null,
+ 'ui.agent.session.expansion.workdir': null,
'settings.provider.last_selected_provider_id': null,
'settings.provider.openai.alert.dismissed': false,
'feature.mcp.is_uv_installed': false,
diff --git a/src/shared/data/preference/__tests__/preferenceSchemas.test.ts b/src/shared/data/preference/__tests__/preferenceSchemas.test.ts
index af651020dae..50f00187260 100644
--- a/src/shared/data/preference/__tests__/preferenceSchemas.test.ts
+++ b/src/shared/data/preference/__tests__/preferenceSchemas.test.ts
@@ -36,13 +36,8 @@ describe('DefaultPreferences', () => {
expect(DefaultPreferences.default['agent.session.display_mode']).toBe(agentSessionDisplayDefault)
})
- it('defaults both conversation and work surfaces to the classic layout for new users', () => {
- const topicLayoutDefault: PreferenceSchemas['default']['topic.layout'] = 'classic'
- const agentLayoutDefault: PreferenceSchemas['default']['agent.layout'] = 'classic'
-
- // preferenceSchemas.ts is generated from classification.json; pin the defaults so a
- // regeneration that drops or flips either layout key fails loudly instead of shipping silently.
- expect(DefaultPreferences.default['topic.layout']).toBe(topicLayoutDefault)
- expect(DefaultPreferences.default['agent.layout']).toBe(agentLayoutDefault)
+ it('does not keep legacy classic/modern layout preferences', () => {
+ expect('topic.layout' in DefaultPreferences.default).toBe(false)
+ expect('agent.layout' in DefaultPreferences.default).toBe(false)
})
})
diff --git a/src/shared/data/preference/preferenceSchemas.ts b/src/shared/data/preference/preferenceSchemas.ts
index dcc0b14ec2d..1710377eaa1 100644
--- a/src/shared/data/preference/preferenceSchemas.ts
+++ b/src/shared/data/preference/preferenceSchemas.ts
@@ -1,6 +1,6 @@
/**
* Auto-generated preferences configuration
- * Generated at: 2026-07-03T04:02:32.176Z
+ * Generated at: 2026-07-06T05:11:12.917Z
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
@@ -37,9 +37,11 @@ import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
export interface PreferenceSchemas {
default: {
// target-key-definitions/complex/complex
- 'agent.layout': PreferenceTypes.ChatLayoutMode
+ 'agent.icon_type': PreferenceTypes.AssistantIconType
// target-key-definitions/complex/complex
'agent.session.display_mode': PreferenceTypes.AgentSessionDisplayMode
+ // target-key-definitions/complex/complex
+ 'agent.session.position': PreferenceTypes.TopicTabPosition
// redux/settings/enableDeveloperMode
'app.developer_mode.enabled': boolean
// redux/settings/autoCheckUpdate
@@ -458,8 +460,6 @@ export interface PreferenceSchemas {
'shortcut.topic.rename': PreferenceTypes.PreferenceShortcutType
// target-key-definitions/complex/complex
'shortcut.topic.sidebar.toggle': PreferenceTypes.PreferenceShortcutType
- // target-key-definitions/complex/complex
- 'topic.layout': PreferenceTypes.ChatLayoutMode
// redux/settings/enableTopicNaming
'topic.naming.enabled': boolean
// target-key-definitions/complex/complex
@@ -468,6 +468,8 @@ export interface PreferenceSchemas {
'topic.naming_prompt': string
// target-key-definitions/complex/complex
'topic.tab.display_mode': PreferenceTypes.TopicDisplayMode
+ // redux/settings/topicPosition
+ 'topic.tab.position': PreferenceTypes.TopicTabPosition
// redux/settings/showTopics
'topic.tab.show': boolean
// redux/settings/customCss
@@ -494,8 +496,9 @@ export interface PreferenceSchemas {
/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
export const DefaultPreferences: PreferenceSchemas = {
default: {
- 'agent.layout': 'classic',
+ 'agent.icon_type': 'emoji',
'agent.session.display_mode': 'workdir',
+ 'agent.session.position': 'left',
'app.developer_mode.enabled': false,
'app.dist.auto_update.enabled': true,
'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel.LATEST,
@@ -732,11 +735,11 @@ export const DefaultPreferences: PreferenceSchemas = {
'shortcut.topic.create': { binding: ['CommandOrControl', 'N'], enabled: true },
'shortcut.topic.rename': { binding: ['CommandOrControl', 'T'], enabled: false },
'shortcut.topic.sidebar.toggle': { binding: ['CommandOrControl', ']'], enabled: true },
- 'topic.layout': 'classic',
'topic.naming.enabled': true,
'topic.naming.model_id': null,
'topic.naming_prompt': '',
'topic.tab.display_mode': 'time',
+ 'topic.tab.position': 'left',
'topic.tab.show': true,
'ui.custom_css': '',
'ui.launchpad.app_order': [],
@@ -759,9 +762,9 @@ export const DefaultPreferences: PreferenceSchemas = {
/**
* 生成统计:
- * - 总配置项: 226
+ * - 总配置项: 227
* - electronStore项: 1
- * - redux项: 173
+ * - redux项: 174
* - localStorage项: 0
* - dexieSettings项: 4
*/
diff --git a/src/shared/data/preference/preferenceTypes.ts b/src/shared/data/preference/preferenceTypes.ts
index a35abba440d..7729b41737c 100644
--- a/src/shared/data/preference/preferenceTypes.ts
+++ b/src/shared/data/preference/preferenceTypes.ts
@@ -84,6 +84,8 @@ export type AssistantTabSortType = 'tags' | 'list'
export type TopicDisplayMode = 'time' | 'assistant'
+export type TopicTabPosition = 'left' | 'right'
+
export type AgentSessionDisplayMode = 'time' | 'agent' | 'workdir'
export const SIDEBAR_FAVORITES = [
@@ -133,9 +135,6 @@ export enum UpgradeChannel {
export type ChatMessageStyle = 'plain' | 'bubble'
-/** Chat resource-list layout: 'classic' = entity rail + right resource panel, 'modern' = single sidebar. */
-export type ChatLayoutMode = 'classic' | 'modern'
-
export type ChatMessageNavigationMode = 'none' | 'buttons' | 'anchor'
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
diff --git a/v2-refactor-temp/docs/breaking-changes/2026-06-26-chat-layout-split-assistant-agent.md b/v2-refactor-temp/docs/breaking-changes/2026-06-26-chat-layout-split-assistant-agent.md
deleted file mode 100644
index e35a3e0b573..00000000000
--- a/v2-refactor-temp/docs/breaking-changes/2026-06-26-chat-layout-split-assistant-agent.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: Assistant and agent chat layouts are separate "classic/modern" layout settings
-category: changed
-severity: notice
-introduced_in_pr: #16434
-date: 2026-06-26
----
-
-## What changed
-
-The single experimental `chat.resource_list.position` (`left`/`right`) preference is replaced by two independent settings under **Settings → Message Settings**:
-
-- **Conversation view** (assistant chats) — `topic.layout`
-- **Work view** (agent chats) — `agent.layout`
-
-Each is **Modern layout** or **Classic layout**:
-
-- **Modern layout** — the single sidebar (topics/sessions listed in the left sidebar).
-- **Classic layout** — a compact assistant/agent entity rail on the left plus the topic/session list in the right panel.
-
-Both settings default to **Classic layout** (the entity rail).
-
-The legacy v1 assistant `topicPosition` is deleted during v2 classification and is not migrated into either setting. Both new settings default to Classic layout.
-
-## Why this matters to the user
-
-Fresh installs and migrated users land on the **Classic layout**: a compact entity rail plus a right-side topic/session panel. Users can switch **Conversation view** or **Work view** to **Modern layout** for the single sidebar.
-
-The preference key also changed: the never-shipped `chat.resource_list.position` no longer exists. Anything expecting that key should read `topic.layout` / `agent.layout` instead.
-
-## What the user should do
-
-Nothing — automatic. Switch **Conversation view** / **Work view** in Settings → Message Settings to change layout.
-
-## Also changed: agent session options menu
-
-The agent session options menu drops its "toggle sidebar" item. This is an alpha behavior change, not data-affecting — documented here for completeness.
-
-## Also changed: default sidebar grouping mode
-
-This PR also changes the **default grouping** of the single-sidebar (Modern layout) lists — an intentional product decision, independent of the layout split:
-
-- `topic.tab.display_mode`: `assistant` → `time`
-- `agent.session.display_mode`: `agent` → `workdir`
-
-Neither key has a v1 migration mapping, so the new default applies to fresh installs **and** users migrating from v1. This is **not** a breaking change — the feature is still in alpha, and existing stored grouping values still render. The current UI does not expose a full display-mode switcher, though: conversation topics have no display-mode menu in the single sidebar, and agent sessions expose only **Time** / **Workdir** choices. Documented here only for completeness.
-
-## Notes for release manager
-
-This entry supersedes the in-development `chat.resource_list.position` default of `right`; that key never shipped to a release, so only the new `classic/modern` framing needs translating.
diff --git a/v2-refactor-temp/docs/chat/chat-layout-modes.md b/v2-refactor-temp/docs/chat/chat-layout-modes.md
index 2157869d621..00b311f1dc5 100644
--- a/v2-refactor-temp/docs/chat/chat-layout-modes.md
+++ b/v2-refactor-temp/docs/chat/chat-layout-modes.md
@@ -1,248 +1,26 @@
-# Chat Layout Modes (classic / modern)
+# Chat Layout Modes
-Each chat surface — assistant conversations (Home) and agent work — has its own
-layout-mode preference. The **classic layout** is a compact entity rail plus a right-side
-resource panel; the **modern layout** is the single sidebar. The `classic` / `modern`
-values are the persisted preference values and feed the `isClassicTopicLayout` /
-`isClassicSessionLayout` flags in the pages.
+Home and Agent no longer persist a separate manual `classic` / `modern` layout
+preference. The layout is derived from the resource-list display mode.
-## Preferences
+## Home
-Two independent preferences, both `'classic' | 'modern'` (`PreferenceTypes.ChatLayoutMode`),
-both defaulting to `'classic'`:
+- `topic.tab.display_mode = 'assistant'` uses the classic layout:
+ assistant rail on the left, chat in the center, topic list in the right pane.
+- `topic.tab.display_mode = 'time'` uses the modern single-sidebar layout.
-- `topic.layout` — assistant chats (Home). v2-only, no v1 source.
-- `agent.layout` — agent chats. v2-only, no v1 source.
+## Agent
-Both are declared in `target-key-definitions.json` and generated into
-`preferenceSchemas.ts`; the legacy v1 `topicPosition` field is deleted during
-classification and is not migrated into either setting. The settings UI
-(`CommonSettings`) exposes them as "Conversation view" and "Work view", each with
-"Classic" / "Modern" labels, under the `settings.messages.layout.classic` / `.modern`
-keys (Chinese: 经典 / 现代).
-
-## Surface terminology
-
-The two surfaces deliberately use different words at different layers; the mapping is
-fixed, not accidental:
-
-| Surface | Domain entity | Resource | Preference namespace | UI label (i18n) |
-| --------- | ------------- | ----------------- | -------------------- | ------------------- |
-| Assistant | `assistant` | topic | `topic.*` | "Conversation view" |
-| Agent | `agent` | session ("work") | `agent.*` | "Work view" |
-
-So `topic.layout` governs the assistant surface and `agent.layout` governs the agent
-surface, while the shared rail/panel components stay entity-generic
-(`ResourceEntityRail`, `ConversationPickerDialog`). The layout *mode* itself is always
-`classic` / `modern` regardless of surface.
-
-## Layout
-
-When the relevant preference is `classic`:
-
-1. Left: a compact assistant/agent entity rail.
-2. Center: the existing chat surface.
-3. Right: an independently toggleable resource panel for the current
- assistant/agent's conversations/works.
-
-When the preference is `modern`, the single sidebar is used
-(`HomeTabs` / `AgentSidePanel`). Its display-mode preferences and logic are kept,
-though the display-mode controls are currently hidden in the UI.
+- `agent.session.display_mode = 'agent'` uses the classic layout:
+ agent rail on the left, chat in the center, session list in the right pane.
+- `agent.session.display_mode = 'time'` or `'workdir'` uses the modern
+ single-sidebar layout.
## State
-- `topic.layout` / `agent.layout` select the mode per surface.
-- `topic.tab.show` controls whether the left entity rail is expanded/collapsed.
-- The classic-layout right panel's open state is persisted per surface via the shared
- `useClassicLayoutRightPaneOpen(surface, isClassic)` hook: `ui.chat.right_pane_open`
- for the assistant surface and `ui.agent.right_pane_open` for the agent surface, so the
- two don't bleed into each other. Home uses it directly on the page-level Shell; Agent
- owns the same cached state in `AgentPage` and threads it through each `AgentChat` Shell
- mount so it survives draft → persistent remounts and page re-entry.
-- Toggling a surface modern → classic → modern restores the last cached classic-layout
- right-panel open state.
-
-## Left entity rail
-
-`ResourceEntityRail` (presentational, generic) + `useResourceEntityRail` (shared
-behavior). The per-variant adapters `AssistantResourceList` / `AgentResourceList`
-own data fetching, pins, deletion, and context menus.
-
-### Scope
-
-- Home shows assistants; Agent shows agents.
-- Only entities that already own conversations/works are shown — visibility is
- derived from the shared resource list (`getResourceParentId`), not a separate
- query.
-- Newly created assistants/agents stay hidden until they have at least one
- topic/session.
-
-### Top action
-
-- Fixed above the sortable entity list: Home adds an assistant, Agent adds an
- agent. Entities cannot be dragged above it.
-- The "+" opens a shared searchable picker (`ConversationPickerDialog`, wrapped
- per surface by `AssistantConversationPickerDialog` / `AgentConversationPickerDialog`)
- to switch to or create an entity. It has a "create new" row and pagination; for
- assistants it filters between 资源库 (the user's assistants) and 助手库 (the
- preset catalog, via `useAssistantCatalogPresets`).
-- A history-records button next to "+" opens the History Records / global-history
- page (`onOpenHistoryRecords`).
-- After creating a new entity the main chat enters its blank state; the entity
- still does not appear in the rail until it owns a topic/session.
-
-### Selection and click behavior
-
-- Selection follows the current assistantId/agentId; if the current entity has no
- resources it has no selected row.
-- `handleSelect` enters the entity's first/most-recent resource (pinned then time
- order via `sortResourcesForEntity`). Because a visible entity always owns at
- least one *loaded* resource, this does **not** wait for the full load — there is
- no dead-click window. The (effectively unreachable) no-resource case falls back
- to a blank draft.
-- Clicking an entity does not open the right panel if it is closed; if it is open
- it stays open and switches to the new entity.
-
-### Pinned entities
-
-- Pinned assistants/agents float into a "已固定" section at the top, mirroring the
- modern layout's left list (entity pins reuse `usePins('assistant'|'agent')`). The rest
- sit under a "助手" / "智能体" section below.
-- Both are collapsible **section** headers (flush-left), so the entity rows keep
- their avatar and read as indented beneath. With nothing pinned the rail renders a
- single flat list with no header — same as the modern layout's single-section case.
-- Pinned rows cannot be dragged and nothing can be dropped into the pinned section;
- only the entities still owning resources appear (the rail's visibility invariant
- is unchanged).
-
-### Ordering & context menu
-
-- Non-pinned entities are ordered by assistant/agent `orderKey`; drag reorders and
- persists the real `orderKey` (optimistic, then refetch).
-- Entity rows keep the single sidebar's entity context menus (assistant grouped-row /
- agent `AgentItem` behavior). Deleting the current entity, or clearing all its
- resources, closes the right panel and leaves the main chat in that entity's
- blank state.
-
-## Right resource panel
-
-The right panel reuses the existing `Shell` right-pane chrome. The topic/session
-list is injected as the first `resources` tab via `ResourcePaneProvider` /
-`useResourcePane` (a context, so the node + label are supplied once at the page
-level instead of prop-threaded).
-
-- Home lists topics ("topic" / "话题"); Agent lists works ("work" / "工作").
-- Lists only the current entity's resources. With no current entity the panel
- opens to an empty list.
-- The panel is mutually exclusive with branch/trace/files/status/flow
- (scoped to the current chat instance). When it is open, the panel header owns the
- close/toggle control.
-- While the right panel is closed, `ConversationShell` mirrors the stable internal
- tab icons into the top-right tool area as tab shortcuts, next to the existing
- right-panel expand button. Clicking a shortcut opens the right panel directly to
- that tab; clicking the expand button opens the default tab. Once the panel is open
- or maximized, the whole top-right tool cluster disappears and the internal tab strip
- owns tab switching / close controls. Dynamic Agent tabs (`flow:*`, `file-preview`)
- stay inside the panel only; the Agent Status shortcut keeps the same hover preview
- as the previous status summary entry.
-- Fixed time grouping, groups expanded by default; does not read/write the
- single sidebar's group-collapsed state or display options. Header keeps only search,
- scoped to the current entity; creating a topic/session stays on the left rail
- and single sidebar entry points. Drag/group movement is disabled (the list is
- fixed time-grouped).
-- Switching assistant/agent clears the right-list search; switching topic/session
- within the same entity does not.
-
-## Composer entity controls (classic layout)
-
-In classic layout the left rail owns entity switching, so the composer's assistant/agent
-switcher is hidden rather than repurposed:
-
-- `ChatComposer` passes `showAssistantTrigger: topicLayout !== 'classic'`.
-- `AgentComposer` passes `showAgentTrigger: sessionLayout !== 'classic'`. The
- `agentTriggerMode="edit"` code path still exists for toolbar-bound contexts, but
- it is not the classic-layout entry point because the trigger is hidden by
- `showAgentTrigger`.
-- Classic layout adds a new conversation/work action to the composer controls when
- `onCreateEmptyTopic` / `onCreateEmptySession` is available.
-- The agent composer's inline model selector remains available for changing the
- active agent model. The agent switcher is hidden inside active sessions (an
- active session is bound to its agent).
-
-## Agent workspace control (classic layout)
-
-Classic-layout agent chats keep the workspace control visible in the composer because
-the classic left sidebar is no longer present:
-
-- Draft sessions keep the existing editable workspace selector.
-- Persistent sessions can switch workspace only while the visible session is still
- empty: messages are loaded, there are no older pages, and the UI message list is
- empty. After any message exists, the same control remains visible as a read-only
- workspace display.
-- Switching patches the current session's workspace source (`user` workspace id or
- `system` / no-project) instead of replacing the draft state. The data service
- rejects workspace updates once the session has messages.
-- The no-project / system workspace is not a user filesystem workspace. It is shown
- as the no-project option, but it does not run directory accessibility preflight
- and is not passed to skills or tool accessible paths. Only `user` workspaces expose
- their path to those checks.
-
-## Data flow
-
-No DataApi endpoint filters topics/sessions by entity — both panes derive from one
-shared full list and filter in the frontend.
-
-- The entity rail and the right panel read the **same** source through
- `useAssistantTopicsSource` / `useAgentSessionsSource`
- (`src/renderer/hooks/resourceViewSources.ts`). These wrap
- `useTopics({ loadAll: true })` / `useSessions(undefined, { loadAll: true,
- pageSize })` so both sides resolve to one SWR key — one fetch, and the load
- options can never drift between the two call sites.
-- `loadAll` is intentional and unavoidable: the rail must know which entities own
- resources, and the panel filters the same list by the current entity. A single
- fetch feeds both.
-- Create/delete/rename/clear/move use the existing mutation/invalidate
- flow; after a mutation the shared source is refreshed once and both sides
- re-derive. No local shadow copies.
-- Assistant/agent metadata supplies display data + operations (name, emoji/avatar,
- `orderKey`, context-menu actions); topic/session data determines visibility.
-- `PATCH /agent-sessions/:sessionId` accepts a normalized `workspace` source for
- empty-session workspace switches. The main service maps it to the backing
- `workspaceId`, creates or removes system workspaces as needed, and refuses the
- update after messages exist.
-
-## Agent pane persistence across the draft→persistent handoff
-
-Home keeps a single page-level `Shell` (via `renderWithRightPane`), so its right
-pane stays open across the draft → persistent topic handoff. The agent chat mounts
-a fresh `AgentRightPane` (= a fresh `Shell`) per conversation branch
-(initializing / draft / missing-agent / persistent), so sending the first message
-in a draft session would otherwise remount the Shell and snap the work panel shut.
-
-To match Home, the `Shell` exposes an additive `onOpenChange` callback;
-`AgentPage` owns the open state (`sessionPaneOpen`) and threads
-`defaultOpen` + `onOpenChange` through `AgentChat` to every `AgentRightPane` mount
-site, so the open state survives the remount. This is scoped to the classic session
-layout (`isClassicSessionLayout`); modern layout passes `undefined` and is byte-for-byte unchanged.
-
-## Key files
-
-- `components/chat/resources/variants/ResourceEntityRail.tsx`,
- `useResourceEntityRail.ts` — rail component + shared behavior.
-- `components/chat/resources/variants/AssistantResourceList.tsx`,
- `AgentResourceList.tsx` — per-variant data adapters.
-- `components/chat/panes/Shell/resourcePane.tsx` — `resources` tab injection.
-- `components/resource/ConversationPickerDialog.tsx` — shared entity/conversation
- picker; wrapped by `pages/home/components/AssistantConversationPickerDialog.tsx`
- and `pages/agents/components/AgentConversationPickerDialog.tsx`.
-- `components/composer/variants/ChatComposer.tsx`,
- `AgentComposer.tsx` — classic-layout entity trigger visibility and new conversation/work
- composer actions.
-- `hooks/useAssistantCatalogPresets.ts` — preset catalog feeding the assistant picker.
-- `hooks/resourceViewSources.ts` — shared full-list sources.
-- `pages/home/HomePage.tsx`, `pages/agents/AgentPage.tsx`,
- `pages/agents/AgentChat.tsx`, `pages/agents/AgentComposerSlot.tsx` — page wiring,
- agent pane persistence, and classic-layout workspace switching.
-- `main/data/services/AgentSessionService.ts`,
- `shared/data/api/schemas/agentSessions.ts` — empty-session workspace update
- validation and persistence.
+- Display mode is stored as Preference data.
+- Classic-layout right pane open state is stored per surface in renderer
+ persist cache: `ui.chat.right_pane_open` for Home and
+ `ui.agent.right_pane_open` for Agent.
+- Resource-list collapsed groups are also stored per display mode in renderer
+ persist cache.
diff --git a/v2-refactor-temp/tools/data-classify/data/classification.json b/v2-refactor-temp/tools/data-classify/data/classification.json
index 99250e9c37e..ea62685c3d3 100644
--- a/v2-refactor-temp/tools/data-classify/data/classification.json
+++ b/v2-refactor-temp/tools/data-classify/data/classification.json
@@ -1037,11 +1037,11 @@
},
{
"originalKey": "topicPosition",
- "type": "string",
+ "type": "PreferenceTypes.TopicTabPosition",
"defaultValue": "left",
- "status": "deleted",
- "category": null,
- "targetKey": null
+ "status": "classified",
+ "category": "preferences",
+ "targetKey": "topic.tab.position"
},
{
"originalKey": "showTopicTime",
diff --git a/v2-refactor-temp/tools/data-classify/data/target-key-definitions.json b/v2-refactor-temp/tools/data-classify/data/target-key-definitions.json
index 2643309092e..1238cb429d9 100644
--- a/v2-refactor-temp/tools/data-classify/data/target-key-definitions.json
+++ b/v2-refactor-temp/tools/data-classify/data/target-key-definitions.json
@@ -103,20 +103,6 @@
"status": "classified",
"description": "Web search provider overrides (from complex migration)"
},
- {
- "targetKey": "topic.layout",
- "type": "PreferenceTypes.ChatLayoutMode",
- "defaultValue": "classic",
- "status": "classified",
- "description": "Assistant conversation layout: 'classic' = entity rail + right topic panel, 'modern' = single topic sidebar (new v2 preference, no v1 source)"
- },
- {
- "targetKey": "agent.layout",
- "type": "PreferenceTypes.ChatLayoutMode",
- "defaultValue": "classic",
- "status": "classified",
- "description": "Agent work layout: 'classic' = entity rail + right session panel, 'modern' = single session sidebar (v2 new feature, no v1 source)"
- },
{
"targetKey": "ui.sidebar.favorites",
"type": "PreferenceTypes.SidebarFavoriteItem[]",
@@ -157,6 +143,13 @@
"status": "classified",
"description": "Topic sidebar display grouping mode"
},
+ {
+ "targetKey": "agent.icon_type",
+ "type": "PreferenceTypes.AssistantIconType",
+ "defaultValue": "emoji",
+ "status": "classified",
+ "description": "Agent rail entity icon style (emoji/model/none), independent of the assistant icon type"
+ },
{
"targetKey": "agent.session.display_mode",
"type": "PreferenceTypes.AgentSessionDisplayMode",
@@ -164,6 +157,13 @@
"status": "classified",
"description": "Agent session sidebar display grouping mode"
},
+ {
+ "targetKey": "agent.session.position",
+ "type": "PreferenceTypes.TopicTabPosition",
+ "defaultValue": "left",
+ "status": "classified",
+ "description": "Agent session resource pane position (left/right), independent of the Home topic pane"
+ },
{
"targetKey": "feature.quick_assistant.model_id",
"type": "string",