feat: pin conversations and float their action menus

Persist conversation pins across restarts and show pinned conversations first, including older history. Anchor action menus above the list without shifting neighboring rows, with keyboard dismissal and sidebar-close cleanup.

Add schema 36 migration, focused regression coverage, and feature documentation. File-level staging also includes existing workbar styles and browser contract adjustments in shared files.

Validation: 145 backend tests and 237 App tests passed; typecheck and lint passed. Full test run timed out with Runtime and Agent package failures; native Electron visual validation remains pending.

Release note: 会话菜单新增置顶和取消置顶,重启后仍保留;操作菜单现在悬浮显示,不再挤动下方会话。
This commit is contained in:
mesalogo
2026-09-15 23:39:06 +08:00
parent b16b0d10e7
commit 625182c3dc
19 changed files with 583 additions and 52 deletions
+33 -4
View File
@@ -9,6 +9,7 @@ import {
conversationAnsweredQuestionSchema,
conversationMessageSchema,
conversationSnapshotSchema,
conversationSetPinnedSchema,
expertCreateSchema,
normalizeInteractiveWorkMode,
persistedProjectExecutionSpaceSchema,
@@ -34,6 +35,7 @@ import type {
ConversationToolActivity,
ConversationMessage,
ConversationSnapshot,
ConversationSetPinnedInput,
ExpertCreateInput,
ExpertUpdateInput,
HeartbeatCreateInput,
@@ -102,7 +104,7 @@ import {
SUBAGENT_PROGRESS_STORAGE_SCHEMA_VERSION
} from './subagent-progress-storage'
export const ASSISTANT_DATABASE_SCHEMA_VERSION = SUBAGENT_PROGRESS_STORAGE_SCHEMA_VERSION
export const ASSISTANT_DATABASE_SCHEMA_VERSION = 36
export type RemoteTaskEventInput = {
taskId: string
@@ -209,6 +211,7 @@ type ConversationRow = {
account_display: string | null
branch_source_conversation_id: string | null
branch_source_title: string | null
pinned: number
updated_at: string
}
@@ -1149,6 +1152,7 @@ function toConversationSnapshot(
}
: {}),
title: conversation.title,
pinned: conversation.pinned === 1,
updatedAt: Date.parse(conversation.updated_at),
// Startup recovery is the only authority for interruption: it persists
// the terminal state once. Deriving it while reading would also hit
@@ -2634,7 +2638,7 @@ export class AssistantDatabase {
external_account_id, external_conversation_id,
conversation_type, account_display,
branch_source_conversation_id, branch_source_title,
updated_at
pinned, updated_at
FROM conversations
WHERE status = 'active'
AND (
@@ -2642,11 +2646,12 @@ export class AssistantDatabase {
ORDER BY updated_at DESC LIMIT 100)
OR id IN (SELECT conversation_id FROM (${activeVisibleTaskSelect}))
OR id IN (SELECT value FROM json_each(?))
OR pinned = 1
OR EXISTS (SELECT 1 FROM messages
WHERE conversation_id = conversations.id
AND state = 'streaming')
)
ORDER BY updated_at DESC`
ORDER BY pinned DESC, updated_at DESC`
)
.all(JSON.stringify([...extraIds])) as ConversationRow[]
}
@@ -2715,7 +2720,7 @@ export class AssistantDatabase {
external_account_id, external_conversation_id,
conversation_type, account_display,
branch_source_conversation_id, branch_source_title,
updated_at
pinned, updated_at
FROM conversations
WHERE id = ? AND status = 'active'`
)
@@ -2740,6 +2745,16 @@ export class AssistantDatabase {
return toConversationSnapshot(conversation, messages)
}
setConversationPinned(input: ConversationSetPinnedInput): void {
const { conversationId, pinned } = conversationSetPinnedSchema.parse(input)
const result = this.requireDatabase().prepare(
`UPDATE conversations SET pinned = ? WHERE id = ? AND status = 'active'`
).run(pinned ? 1 : 0, conversationId)
if (result.changes !== 1) {
throw new Error('对话不存在')
}
}
repairConversationRuntimeSelections(
settings: RuntimeSelectionRepairSettings
): number {
@@ -9916,6 +9931,20 @@ export class AssistantDatabase {
// Startup's worker has already compacted full and schema-34 delta events.
database.exec(`PRAGMA user_version = ${SUBAGENT_PROGRESS_STORAGE_SCHEMA_VERSION}`)
}
if (version.user_version < 36) {
database.exec('BEGIN IMMEDIATE')
try {
database.exec(`
ALTER TABLE conversations ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0
CHECK(pinned IN (0, 1));
PRAGMA user_version = 36;
COMMIT;
`)
} catch (error) {
database.exec('ROLLBACK')
throw error
}
}
}
private requireDatabase(): DatabaseSync {