feat: add persistent desktop assistant workspace

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-31 22:33:03 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 698a15ad14
commit 6ef1795b81
101 changed files with 31866 additions and 1176 deletions
+338 -5
View File
@@ -1,15 +1,39 @@
import { contextBridge, ipcRenderer } from 'electron'
import { contextBridge, ipcRenderer, webUtils } from 'electron'
import {
type ApprovalDecision,
type AgentEvent,
type AgentRequest,
type AgentRuntimeDetection,
type AgentRuntimeStatus,
type AppInfo,
type ContextAttachment,
type DesktopApi,
type KnowledgeLibrary,
type KnowledgeSearchReference,
type KnowledgeSnapshot,
type RuntimeSettings,
type RuntimeSettingsInput
type RuntimeSettingsInput,
type RuntimeFileSelectionKind
} from '../shared/contracts'
import { ipcChannels } from '../shared/ipc-channels'
import type {
CapabilitySnapshot,
McpServerTestResult
} from '../shared/capability-contracts'
import type {
AssistantProject,
AssistantArtifact,
AssistantMemory,
AssistantSchedule,
AssistantExpert,
AssistantTask,
ConversationSnapshot,
WorkspaceChanges,
ProjectCreateInput,
MemoryCreateInput,
ScheduleCreateInput,
ExpertCreateInput
} from '../shared/assistant-contracts'
const desktopApi: DesktopApi = {
app: {
@@ -24,6 +48,11 @@ const desktopApi: DesktopApi = {
const handler = (): void => listener()
ipcRenderer.on(ipcChannels.conversationNew, handler)
return () => ipcRenderer.removeListener(ipcChannels.conversationNew, handler)
},
onOpenSettings: (listener) => {
const handler = (): void => listener()
ipcRenderer.on(ipcChannels.settingsOpen, handler)
return () => ipcRenderer.removeListener(ipcChannels.settingsOpen, handler)
}
},
agent: {
@@ -37,10 +66,13 @@ const desktopApi: DesktopApi = {
cancel: async (requestId: string) => {
await ipcRenderer.invoke(ipcChannels.agentCancel, requestId)
},
respondApproval: async (approvalId: string, approved: boolean) => {
respondApproval: async (
approvalId: string,
decision: ApprovalDecision
) => {
await ipcRenderer.invoke(ipcChannels.agentApprovalRespond, {
approvalId,
approved
decision
})
},
onEvent: (listener) => {
@@ -59,16 +91,317 @@ const desktopApi: DesktopApi = {
ipcRenderer.invoke(
ipcChannels.runtimeSettingsUpdate,
input
) as Promise<RuntimeSettings>
) as Promise<RuntimeSettings>,
selectWorkspace: () =>
ipcRenderer.invoke(
ipcChannels.runtimeSettingsSelectWorkspace
) as Promise<string | undefined>,
detectAgentRuntimes: () =>
ipcRenderer.invoke(
ipcChannels.runtimeSettingsDetect
) as Promise<AgentRuntimeDetection>,
selectRuntimeFile: (kind: RuntimeFileSelectionKind) =>
ipcRenderer.invoke(
ipcChannels.runtimeSettingsSelectFile,
kind
) as Promise<string | undefined>,
testRuntime: () =>
ipcRenderer.invoke(
ipcChannels.runtimeSettingsTest
) as Promise<AgentRuntimeStatus>
},
projects: {
list: (includeArchived = false) =>
ipcRenderer.invoke(
ipcChannels.projectsList,
includeArchived
) as Promise<AssistantProject[]>,
create: (input: ProjectCreateInput) =>
ipcRenderer.invoke(
ipcChannels.projectsCreate,
input
) as Promise<AssistantProject>,
update: (projectId: string, input: ProjectCreateInput) =>
ipcRenderer.invoke(
ipcChannels.projectsUpdate,
{ projectId, input }
) as Promise<AssistantProject>,
setArchived: async (projectId: string, archived: boolean) => {
await ipcRenderer.invoke(ipcChannels.projectsSetArchived, {
projectId,
archived
})
}
},
conversations: {
list: () =>
ipcRenderer.invoke(
ipcChannels.conversationsList
) as Promise<ConversationSnapshot[]>,
replace: async (conversations: ConversationSnapshot[]) => {
await ipcRenderer.invoke(
ipcChannels.conversationsReplace,
conversations
)
}
},
workspace: {
getChanges: (projectId: string) =>
ipcRenderer.invoke(
ipcChannels.workspaceChangesGet,
projectId
) as Promise<WorkspaceChanges>
},
tasks: {
list: () =>
ipcRenderer.invoke(ipcChannels.tasksList) as Promise<AssistantTask[]>
},
artifacts: {
list: (projectId?: string) =>
ipcRenderer.invoke(
ipcChannels.artifactsList,
projectId
) as Promise<AssistantArtifact[]>,
importFiles: (projectId?: string) =>
ipcRenderer.invoke(
ipcChannels.artifactsImportFiles,
projectId
) as Promise<AssistantArtifact[]>
},
memory: {
list: (scopeId?: string) =>
ipcRenderer.invoke(
ipcChannels.memoryList,
scopeId
) as Promise<AssistantMemory[]>,
create: (input: MemoryCreateInput) =>
ipcRenderer.invoke(
ipcChannels.memoryCreate,
input
) as Promise<AssistantMemory>,
setStatus: async (
memoryId: string,
status: AssistantMemory['status']
) => {
await ipcRenderer.invoke(ipcChannels.memorySetStatus, {
memoryId,
status
})
},
remove: async (memoryId: string) => {
await ipcRenderer.invoke(ipcChannels.memoryRemove, memoryId)
}
},
schedules: {
list: (projectId?: string) =>
ipcRenderer.invoke(
ipcChannels.schedulesList,
projectId
) as Promise<AssistantSchedule[]>,
create: (input: ScheduleCreateInput) =>
ipcRenderer.invoke(
ipcChannels.schedulesCreate,
input
) as Promise<AssistantSchedule>,
setEnabled: async (scheduleId: string, enabled: boolean) => {
await ipcRenderer.invoke(ipcChannels.schedulesSetEnabled, {
scheduleId,
enabled
})
},
remove: async (scheduleId: string) => {
await ipcRenderer.invoke(ipcChannels.schedulesRemove, scheduleId)
},
runNow: async (scheduleId: string) => {
await ipcRenderer.invoke(ipcChannels.schedulesRunNow, scheduleId)
}
},
experts: {
list: () =>
ipcRenderer.invoke(
ipcChannels.expertsList
) as Promise<AssistantExpert[]>,
create: (input: ExpertCreateInput) =>
ipcRenderer.invoke(
ipcChannels.expertsCreate,
input
) as Promise<AssistantExpert>
},
capabilities: {
getSnapshot: () =>
ipcRenderer.invoke(
ipcChannels.capabilitiesSnapshot
) as Promise<CapabilitySnapshot>,
importSkill: () =>
ipcRenderer.invoke(
ipcChannels.capabilitiesImportSkill
) as Promise<CapabilitySnapshot>,
removeSkill: (skillId) =>
ipcRenderer.invoke(
ipcChannels.capabilitiesRemoveSkill,
skillId
) as Promise<CapabilitySnapshot>,
setSkillEnabled: (skillId, enabled) =>
ipcRenderer.invoke(ipcChannels.capabilitiesToggleSkill, {
skillId,
enabled
}) as Promise<CapabilitySnapshot>,
setSkillAssignments: (skillId, assignments) =>
ipcRenderer.invoke(ipcChannels.capabilitiesAssignSkill, {
skillId,
assignments
}) as Promise<CapabilitySnapshot>,
saveMcpServer: (serverId, input) =>
ipcRenderer.invoke(ipcChannels.capabilitiesSaveMcp, {
serverId,
input
}) as Promise<CapabilitySnapshot>,
removeMcpServer: (serverId) =>
ipcRenderer.invoke(
ipcChannels.capabilitiesRemoveMcp,
serverId
) as Promise<CapabilitySnapshot>,
testMcpServer: (serverId) =>
ipcRenderer.invoke(
ipcChannels.capabilitiesTestMcp,
serverId
) as Promise<McpServerTestResult>
},
context: {
selectFiles: () =>
ipcRenderer.invoke(
ipcChannels.contextSelectFiles
) as Promise<ContextAttachment[]>,
captureScreen: () =>
ipcRenderer.invoke(
ipcChannels.contextCaptureScreen
) as Promise<ContextAttachment>,
captureWindow: () =>
ipcRenderer.invoke(
ipcChannels.contextCaptureWindow
) as Promise<ContextAttachment>,
readClipboard: () =>
ipcRenderer.invoke(
ipcChannels.contextReadClipboard
) as Promise<ContextAttachment>,
remove: async (contextId: string) => {
await ipcRenderer.invoke(ipcChannels.contextRemove, contextId)
}
},
knowledge: {
getSnapshot: (libraryId?: string) =>
ipcRenderer.invoke(
ipcChannels.knowledgeSnapshot,
libraryId
) as Promise<KnowledgeSnapshot>,
createLibrary: (input) =>
ipcRenderer.invoke(
ipcChannels.knowledgeCreateLibrary,
input
) as Promise<KnowledgeLibrary>,
updateLibrary: async (libraryId, update) => {
await ipcRenderer.invoke(ipcChannels.knowledgeUpdateLibrary, {
libraryId,
...update
})
},
deleteLibrary: async (libraryId) => {
await ipcRenderer.invoke(
ipcChannels.knowledgeDeleteLibrary,
libraryId
)
},
selectFiles: async (libraryId, graphStrategy) => {
await ipcRenderer.invoke(ipcChannels.knowledgeSelectFiles, {
libraryId,
graphStrategy
})
},
selectDirectory: async (libraryId, graphStrategy) => {
await ipcRenderer.invoke(
ipcChannels.knowledgeSelectDirectory,
{ libraryId, graphStrategy }
)
},
importDroppedFiles: async (libraryId, files, graphStrategy) => {
const paths = files
.map((file) => webUtils.getPathForFile(file))
.filter(Boolean)
await ipcRenderer.invoke(ipcChannels.knowledgeImportPaths, {
libraryId,
paths,
graphStrategy
})
},
importUrl: async (libraryId, url, graphStrategy) => {
await ipcRenderer.invoke(ipcChannels.knowledgeImportUrl, {
libraryId,
url,
graphStrategy
})
},
syncSource: async (sourceId) => {
await ipcRenderer.invoke(ipcChannels.knowledgeSyncSource, sourceId)
},
pauseSource: async (sourceId) => {
await ipcRenderer.invoke(ipcChannels.knowledgePauseSource, sourceId)
},
retrySource: async (sourceId) => {
await ipcRenderer.invoke(ipcChannels.knowledgeRetrySource, sourceId)
},
removeSource: async (sourceId) => {
await ipcRenderer.invoke(ipcChannels.knowledgeRemoveSource, sourceId)
},
search: (libraryIds, query) =>
ipcRenderer.invoke(ipcChannels.knowledgeSearch, {
libraryIds,
query
}) as Promise<KnowledgeSearchReference[]>,
createEntity: async (libraryId, input) => {
await ipcRenderer.invoke(ipcChannels.knowledgeCreateEntity, {
libraryId,
input
})
},
updateEntity: async (entityId, update) => {
await ipcRenderer.invoke(ipcChannels.knowledgeUpdateEntity, {
entityId,
update
})
},
moveEntity: async (entityId, position) => {
await ipcRenderer.invoke(ipcChannels.knowledgeMoveEntity, {
entityId,
position
})
},
deleteEntity: async (entityId) => {
await ipcRenderer.invoke(ipcChannels.knowledgeDeleteEntity, entityId)
},
mergeEntities: async (sourceEntityId, targetEntityId) => {
await ipcRenderer.invoke(ipcChannels.knowledgeMergeEntities, {
sourceEntityId,
targetEntityId
})
},
createRelation: async (libraryId, input) => {
await ipcRenderer.invoke(ipcChannels.knowledgeCreateRelation, {
libraryId,
input
})
},
updateRelation: async (relationId, input) => {
await ipcRenderer.invoke(ipcChannels.knowledgeUpdateRelation, {
relationId,
input
})
},
deleteRelation: async (relationId) => {
await ipcRenderer.invoke(
ipcChannels.knowledgeDeleteRelation,
relationId
)
}
}
}