fix(hololake): avoid no-op agent workspace writes
This commit is contained in:
parent
392d205341
commit
dc1a32a52b
6 changed files with 72 additions and 5 deletions
|
|
@ -54,6 +54,8 @@ pnpm package:internal:windows
|
||||||
|
|
||||||
内测 Mac 包是 ad-hoc 签名、未公证;Windows 包在没有商业证书时未做 Authenticode 签名。交付时必须明确说明,不能描述成正式签名发行版。
|
内测 Mac 包是 ad-hoc 签名、未公证;Windows 包在没有商业证书时未做 Authenticode 签名。交付时必须明确说明,不能描述成正式签名发行版。
|
||||||
|
|
||||||
|
`tauri.local-candidate.conf.json` 的独立 bundle identifier 只保证候选应用可以与已安装版本并存,不隔离运行数据。当前架构仍从共享的 `com.tolaria.app` 命名空间读取设置、挂载知识库和 Agent 会话。需要无扰动验收时,必须用临时 `XDG_CONFIG_HOME` 启动候选,并在临时配置副本上验证;不能把“没有点击保存”当成“没有写盘”。
|
||||||
|
|
||||||
## Windows 构建恢复顺序
|
## Windows 构建恢复顺序
|
||||||
|
|
||||||
1. `pnpm install --frozen-lockfile`
|
1. `pnpm install --frozen-lockfile`
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import test from 'node:test'
|
||||||
|
|
||||||
const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
|
const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
|
||||||
|
|
||||||
test('local native candidate has an isolated macOS identity', async () => {
|
test('local native candidate has an isolated macOS package identity', async () => {
|
||||||
const [base, candidate, packageJson] = await Promise.all([
|
const [base, candidate, packageJson] = await Promise.all([
|
||||||
readJson('src-tauri/tauri.conf.json'),
|
readJson('src-tauri/tauri.conf.json'),
|
||||||
readJson('src-tauri/tauri.local-candidate.conf.json'),
|
readJson('src-tauri/tauri.local-candidate.conf.json'),
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,30 @@ describe('AiWorkspace', () => {
|
||||||
expect(screen.queryByRole('button', { name: 'Current work' })).toBeNull()
|
expect(screen.queryByRole('button', { name: 'Current work' })).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not rewrite unchanged conversation settings when the workspace opens', async () => {
|
||||||
|
const onConversationSettingsChange = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AiWorkspace
|
||||||
|
open
|
||||||
|
mode="side"
|
||||||
|
aiAgentsStatus={installedStatuses()}
|
||||||
|
aiModelProviders={providers}
|
||||||
|
conversationSettings={[
|
||||||
|
{ id: 'live-chat', title: 'Current work', target_id: null, archived: false },
|
||||||
|
]}
|
||||||
|
vaultPath="/tmp/vault"
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onConversationSettingsChange={onConversationSettingsChange}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('button', { name: 'Current work' })).toBeTruthy()
|
||||||
|
})
|
||||||
|
expect(onConversationSettingsChange).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('recovers stored sessions that no longer have conversation metadata', async () => {
|
it('recovers stored sessions that no longer have conversation metadata', async () => {
|
||||||
setAiWorkspaceSessionMessages('orphaned-chat', [{
|
setAiWorkspaceSessionMessages('orphaned-chat', [{
|
||||||
userMessage: 'Recovered prompt',
|
userMessage: 'Recovered prompt',
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,20 @@ function conversationsToSettings(conversations: AiConversation[]): AiWorkspaceCo
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function conversationSettingsEqual(
|
||||||
|
current: AiWorkspaceConversationSetting[],
|
||||||
|
stored: AiWorkspaceConversationSetting[] | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!stored || current.length !== stored.length) return false
|
||||||
|
return current.every((conversation, index) => {
|
||||||
|
const persisted = stored[index]
|
||||||
|
return conversation.archived === (persisted.archived === true)
|
||||||
|
&& conversation.id === persisted.id
|
||||||
|
&& conversation.target_id === (persisted.target_id?.trim() || null)
|
||||||
|
&& conversation.title === persisted.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function activeConversationForState(
|
export function activeConversationForState(
|
||||||
conversations: AiConversation[],
|
conversations: AiConversation[],
|
||||||
activeId: ConversationId,
|
activeId: ConversationId,
|
||||||
|
|
@ -370,10 +384,12 @@ function initialActiveId(conversations: AiConversation[], requestedId: Conversat
|
||||||
function useConversationSettingsPersistence({
|
function useConversationSettingsPersistence({
|
||||||
conversations,
|
conversations,
|
||||||
onSettingsChange,
|
onSettingsChange,
|
||||||
|
settings,
|
||||||
settingsReady,
|
settingsReady,
|
||||||
}: {
|
}: {
|
||||||
conversations: AiConversation[]
|
conversations: AiConversation[]
|
||||||
onSettingsChange?: (conversations: AiWorkspaceConversationSetting[]) => void
|
onSettingsChange?: (conversations: AiWorkspaceConversationSetting[]) => void
|
||||||
|
settings?: AiWorkspaceConversationSetting[] | null
|
||||||
settingsReady: boolean
|
settingsReady: boolean
|
||||||
}) {
|
}) {
|
||||||
const onSettingsChangeRef = useRef(onSettingsChange)
|
const onSettingsChangeRef = useRef(onSettingsChange)
|
||||||
|
|
@ -384,8 +400,10 @@ function useConversationSettingsPersistence({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!settingsReady) return
|
if (!settingsReady) return
|
||||||
onSettingsChangeRef.current?.(conversationsToSettings(conversations))
|
const nextSettings = conversationsToSettings(conversations)
|
||||||
}, [conversations, settingsReady])
|
if (conversationSettingsEqual(nextSettings, settings)) return
|
||||||
|
onSettingsChangeRef.current?.(nextSettings)
|
||||||
|
}, [conversations, settings, settingsReady])
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTitleConversationFromAnswer(setConversations: SetConversations) {
|
function useTitleConversationFromAnswer(setConversations: SetConversations) {
|
||||||
|
|
@ -490,7 +508,7 @@ export function useConversations({
|
||||||
|
|
||||||
const titleConversationFromAnswer = useTitleConversationFromAnswer(setConversations)
|
const titleConversationFromAnswer = useTitleConversationFromAnswer(setConversations)
|
||||||
const updateDefaultConversationTargets = useUpdateDefaultConversationTargets(setConversations)
|
const updateDefaultConversationTargets = useUpdateDefaultConversationTargets(setConversations)
|
||||||
useConversationSettingsPersistence({ conversations, onSettingsChange, settingsReady })
|
useConversationSettingsPersistence({ conversations, onSettingsChange, settings, settingsReady })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation,
|
activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation,
|
||||||
|
|
|
||||||
|
|
@ -178,4 +178,28 @@ describe('aiWorkspaceSessionStore', () => {
|
||||||
expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1)
|
expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not rewrite native history when hydration only reads it', async () => {
|
||||||
|
isTauriState.value = true
|
||||||
|
invokeMock.mockImplementation(async (command: string) => {
|
||||||
|
if (command === 'get_ai_workspace_sessions') {
|
||||||
|
return {
|
||||||
|
'native-chat': {
|
||||||
|
messages: [{ userMessage: 'Native', actions: [], response: 'History', id: 'native-message' }],
|
||||||
|
status: 'done',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = await import('./aiWorkspaceSessionStore')
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1)
|
||||||
|
})
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300))
|
||||||
|
|
||||||
|
expect(invokeMock).not.toHaveBeenCalledWith('save_ai_workspace_sessions', expect.anything())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ async function syncFromNativeStorage(): Promise<void> {
|
||||||
const mergedSessions = mergeStoredSessions(sessionStore.getSnapshot(), nativeSessions)
|
const mergedSessions = mergeStoredSessions(sessionStore.getSnapshot(), nativeSessions)
|
||||||
sessionStore.replaceSnapshot(mergedSessions)
|
sessionStore.replaceSnapshot(mergedSessions)
|
||||||
sessionStore.writeStoredSnapshot(mergedSessions)
|
sessionStore.writeStoredSnapshot(mergedSessions)
|
||||||
if (Object.keys(mergedSessions).length > 0) scheduleNativeSessionsWrite(mergedSessions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureSessionStoreSync(): void {
|
function ensureSessionStoreSync(): void {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue