Skip to content

Commit f516e16

Browse files
zhangmo8zerob13
andauthored
refactor(model): derive selectable model source (#1575)
* refactor(model): derive selectable model source * refactor(model): unify selection resolver (#1576) * refactor(chat): split acp status bar state (#1578) * refactor(model): unify selection resolver * refactor(chat): extract acp status bar state * perf(agent): refresh acp agents incrementally (#1579) * fix(provider): gc removed model state (#1577) * fix: address pr review feedback --------- Co-authored-by: zerob13 <zerob13@gmail.com>
1 parent a863686 commit f516e16

28 files changed

Lines changed: 1785 additions & 690 deletions

src/main/presenter/configPresenter/index.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,14 @@ export class ConfigPresenter implements IConfigPresenter {
508508
setModelStatus: this.modelStatusHelper.setModelStatus.bind(this.modelStatusHelper),
509509
deleteModelStatus: this.modelStatusHelper.deleteModelStatus.bind(this.modelStatusHelper)
510510
})
511+
this.providerHelper.setCleanupHooks({
512+
deleteProviderModelStatuses: this.modelStatusHelper.deleteProviderModelStatuses.bind(
513+
this.modelStatusHelper
514+
),
515+
clearProviderModelStore: this.providerModelHelper.clearProviderModelStore.bind(
516+
this.providerModelHelper
517+
)
518+
})
511519

512520
// Initialize built-in ACP agents on first run or version upgrade
513521
// Initialize provider models directory
@@ -1989,15 +1997,15 @@ export class ConfigPresenter implements IConfigPresenter {
19891997
error: null
19901998
}
19911999
this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, installingState)
1992-
this.notifyAcpAgentsChanged()
2000+
this.notifyAcpAgentsChanged([registryAgent.id])
19932001

19942002
try {
19952003
const installedState = await this.acpLaunchSpecService.ensureRegistryAgentInstalled(
19962004
registryAgent,
19972005
currentState
19982006
)
19992007
this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, installedState)
2000-
this.notifyAcpAgentsChanged()
2008+
this.handleAcpAgentsMutated([registryAgent.id])
20012009
return installedState
20022010
} catch (error) {
20032011
const failedState: AcpAgentInstallState = {
@@ -2011,7 +2019,7 @@ export class ConfigPresenter implements IConfigPresenter {
20112019
error: error instanceof Error ? error.message : String(error)
20122020
}
20132021
this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, failedState)
2014-
this.notifyAcpAgentsChanged()
2022+
this.notifyAcpAgentsChanged([registryAgent.id])
20152023
throw error
20162024
}
20172025
}
@@ -2030,7 +2038,7 @@ export class ConfigPresenter implements IConfigPresenter {
20302038
error: null
20312039
}
20322040
this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, repairingState)
2033-
this.notifyAcpAgentsChanged()
2041+
this.notifyAcpAgentsChanged([registryAgent.id])
20342042

20352043
try {
20362044
const installedState = await this.acpLaunchSpecService.ensureRegistryAgentInstalled(
@@ -2053,7 +2061,7 @@ export class ConfigPresenter implements IConfigPresenter {
20532061
error: error instanceof Error ? error.message : String(error)
20542062
}
20552063
this.getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id, failedState)
2056-
this.notifyAcpAgentsChanged()
2064+
this.notifyAcpAgentsChanged([registryAgent.id])
20572065
throw error
20582066
}
20592067
}
@@ -2301,7 +2309,7 @@ export class ConfigPresenter implements IConfigPresenter {
23012309

23022310
private handleAcpAgentsMutated(agentIds?: string[]) {
23032311
this.clearProviderModelStatusCache('acp')
2304-
this.notifyAcpAgentsChanged()
2312+
this.notifyAcpAgentsChanged(agentIds)
23052313
this.refreshAcpProviderAgents(agentIds)
23062314
}
23072315

@@ -2323,10 +2331,10 @@ export class ConfigPresenter implements IConfigPresenter {
23232331
}
23242332
}
23252333

2326-
private notifyAcpAgentsChanged() {
2334+
private notifyAcpAgentsChanged(agentIds?: string[]) {
23272335
console.log('[ACP] notifyAcpAgentsChanged: sending MODEL_LIST_CHANGED event for provider "acp"')
23282336
eventBus.send(CONFIG_EVENTS.MODEL_LIST_CHANGED, SendTarget.ALL_WINDOWS, 'acp')
2329-
eventBus.send(CONFIG_EVENTS.AGENTS_CHANGED, SendTarget.ALL_WINDOWS)
2337+
eventBus.send(CONFIG_EVENTS.AGENTS_CHANGED, SendTarget.ALL_WINDOWS, { agentIds })
23302338
eventBus.sendToRenderer(SESSION_EVENTS.LIST_UPDATED, SendTarget.ALL_WINDOWS)
23312339
}
23322340

src/main/presenter/configPresenter/modelStatusHelper.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class ModelStatusHelper {
2727
return `${MODEL_STATUS_KEY_PREFIX}${providerId}_${formattedModelId}`
2828
}
2929

30-
private buildStatusSnapshot(): Map<string, boolean> | null {
30+
private getRawStoreEntries(): [string, unknown][] | null {
3131
const candidate = this.store as ElectronStore<any> & {
3232
store?: Record<string, unknown>
3333
}
@@ -36,8 +36,17 @@ export class ModelStatusHelper {
3636
return null
3737
}
3838

39+
return Object.entries(rawStore)
40+
}
41+
42+
private buildStatusSnapshot(): Map<string, boolean> | null {
43+
const rawEntries = this.getRawStoreEntries()
44+
if (!rawEntries) {
45+
return null
46+
}
47+
3948
const snapshot = new Map<string, boolean>()
40-
for (const [key, value] of Object.entries(rawStore)) {
49+
for (const [key, value] of rawEntries) {
4150
if (!key.startsWith(MODEL_STATUS_KEY_PREFIX)) {
4251
continue
4352
}
@@ -231,4 +240,39 @@ export class ModelStatusHelper {
231240
this.cache.delete(statusKey)
232241
this.statusSnapshot?.delete(statusKey)
233242
}
243+
244+
deleteProviderModelStatuses(providerId: string): void {
245+
const prefix = `${MODEL_STATUS_KEY_PREFIX}${providerId}_`
246+
const keysToDelete = new Set<string>()
247+
248+
const rawEntries = this.getRawStoreEntries()
249+
if (rawEntries) {
250+
for (const [key] of rawEntries) {
251+
if (key.startsWith(prefix)) {
252+
keysToDelete.add(key)
253+
}
254+
}
255+
}
256+
257+
const statusSnapshot = this.getStatusSnapshot()
258+
if (statusSnapshot) {
259+
for (const key of statusSnapshot.keys()) {
260+
if (key.startsWith(prefix)) {
261+
keysToDelete.add(key)
262+
}
263+
}
264+
}
265+
266+
for (const key of this.cache.keys()) {
267+
if (key.startsWith(prefix)) {
268+
keysToDelete.add(key)
269+
}
270+
}
271+
272+
for (const key of keysToDelete) {
273+
this.store.delete(key)
274+
}
275+
276+
this.clearProviderModelStatusCache(providerId)
277+
}
234278
}

src/main/presenter/configPresenter/providerHelper.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,27 @@ interface ProviderHelperOptions {
1818
defaultProviders: LLM_PROVIDER[]
1919
}
2020

21+
interface ProviderCleanupHooks {
22+
deleteProviderModelStatuses?: (providerId: string) => void
23+
clearProviderModelStore?: (providerId: string) => void
24+
}
25+
2126
export class ProviderHelper {
2227
private readonly store: ElectronStore<any>
2328
private readonly setSetting: SetSetting
2429
private readonly defaultProviders: LLM_PROVIDER[]
30+
private cleanupHooks: ProviderCleanupHooks = {}
2531

2632
constructor(options: ProviderHelperOptions) {
2733
this.store = options.store
2834
this.setSetting = options.setSetting
2935
this.defaultProviders = options.defaultProviders
3036
}
3137

38+
setCleanupHooks(hooks: ProviderCleanupHooks): void {
39+
this.cleanupHooks = hooks
40+
}
41+
3242
getProviders(): LLM_PROVIDER[] {
3343
const providers = this.store.get(PROVIDERS_STORE_KEY) as LLM_PROVIDER[] | undefined
3444

@@ -196,6 +206,18 @@ export class ProviderHelper {
196206
const filteredProviders = providers.filter((p) => p.id !== providerId)
197207
this.setSetting<LLM_PROVIDER[]>(PROVIDERS_STORE_KEY, filteredProviders)
198208

209+
try {
210+
this.cleanupHooks.deleteProviderModelStatuses?.(providerId)
211+
} catch (error) {
212+
console.error(`[Config] Failed to delete model statuses for ${providerId}:`, error)
213+
}
214+
215+
try {
216+
this.cleanupHooks.clearProviderModelStore?.(providerId)
217+
} catch (error) {
218+
console.error(`[Config] Failed to clear provider model store for ${providerId}:`, error)
219+
}
220+
199221
const change: ProviderChange = {
200222
operation: 'remove',
201223
providerId,

src/main/presenter/configPresenter/providerModelHelper.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,22 @@ export class ProviderModelHelper {
238238
this.invalidateProviderModelsCache(providerId)
239239
}
240240

241+
clearProviderModelStore(providerId: string): void {
242+
const store = this.getProviderModelStore(providerId) as ElectronStore<IModelStore> & {
243+
clear?: () => void
244+
}
245+
246+
if (typeof store.clear === 'function') {
247+
store.clear()
248+
} else {
249+
store.set('models', [])
250+
store.set('custom_models', [])
251+
}
252+
253+
this.stores.delete(providerId)
254+
this.invalidateProviderModelsCache(providerId)
255+
}
256+
241257
addCustomModel(providerId: string, model: MODEL_META): void {
242258
const models = this.getCustomModels(providerId)
243259
const existingIndex = models.findIndex((m) => m.id === model.id)

src/main/routes/config/configRouteHandler.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
configGetSystemPromptsRoute,
2525
configGetThemeRoute,
2626
configGetVoiceAiConfigRoute,
27+
configListAgentsRoute,
2728
configListCustomPromptsRoute,
2829
configResetDefaultSystemPromptRoute,
2930
configResetShortcutKeysRoute,
@@ -320,6 +321,25 @@ export async function dispatchConfigRoute(
320321
return configGetAcpStateRoute.output.parse(await readAcpState(configPresenter))
321322
}
322323

324+
case configListAgentsRoute.name: {
325+
const input = configListAgentsRoute.input.parse(rawInput)
326+
const idSet = input.ids ? new Set(input.ids) : null
327+
const agents = (await configPresenter.listAgents()).filter((agent) => {
328+
const agentType = agent.agentType ?? agent.type
329+
if (input.agentType && agentType !== input.agentType) {
330+
return false
331+
}
332+
333+
if (idSet && !idSet.has(agent.id)) {
334+
return false
335+
}
336+
337+
return true
338+
})
339+
340+
return configListAgentsRoute.output.parse({ agents })
341+
}
342+
323343
case configResolveDeepChatAgentConfigRoute.name: {
324344
const input = configResolveDeepChatAgentConfigRoute.input.parse(rawInput)
325345
return configResolveDeepChatAgentConfigRoute.output.parse({

src/main/routes/legacyTypedEventBridge.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ export function setupLegacyTypedEventBridge(deps: {
5353
})
5454
}
5555

56-
const publishAgentsChanged = async () => {
56+
const publishAgentsChanged = async (agentIds?: string[]) => {
5757
const state = await readAcpState(configPresenter)
5858
publishDeepchatEvent('config.agents.changed', {
5959
...state,
60+
agentIds,
6061
version: Date.now()
6162
})
6263
}
@@ -173,8 +174,8 @@ export function setupLegacyTypedEventBridge(deps: {
173174
})
174175
})
175176

176-
eventBus.on(CONFIG_EVENTS.AGENTS_CHANGED, () => {
177-
void publishAgentsChanged()
177+
eventBus.on(CONFIG_EVENTS.AGENTS_CHANGED, (payload?: { agentIds?: string[] }) => {
178+
void publishAgentsChanged(payload?.agentIds)
178179
publishDeepchatEvent('models.changed', {
179180
reason: 'agents',
180181
providerId: 'acp',

src/renderer/api/ConfigClient.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
configGetSystemPromptsRoute,
3636
configGetThemeRoute,
3737
configGetVoiceAiConfigRoute,
38+
configListAgentsRoute,
3839
configListCustomPromptsRoute,
3940
configResetDefaultSystemPromptRoute,
4041
configResetShortcutKeysRoute,
@@ -66,6 +67,7 @@ import type {
6667
ShortcutKeySetting,
6768
SystemPrompt
6869
} from '@shared/presenter'
70+
import type { Agent } from '@shared/types/agent-interface'
6971
import { getDeepchatBridge } from './core'
7072
import { createSettingsClient } from './SettingsClient'
7173

@@ -313,6 +315,14 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge())
313315

314316
type AcpAgents = Awaited<ReturnType<typeof getAcpAgents>>
315317

318+
async function listAgents(input?: {
319+
agentType?: 'deepchat' | 'acp'
320+
ids?: string[]
321+
}): Promise<Agent[]> {
322+
const result = await bridge.invoke(configListAgentsRoute.name, input ?? {})
323+
return result.agents
324+
}
325+
316326
async function resolveDeepChatAgentConfig(agentId: string) {
317327
const result = await bridge.invoke(configResolveDeepChatAgentConfigRoute.name, {
318328
agentId
@@ -450,7 +460,12 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge())
450460
}
451461

452462
function onAgentsChanged(
453-
listener: (payload: { enabled: boolean; agents: AcpAgents; version: number }) => void
463+
listener: (payload: {
464+
enabled: boolean
465+
agents: AcpAgents
466+
agentIds?: string[]
467+
version: number
468+
}) => void
454469
) {
455470
return bridge.on(configAgentsChangedEvent.name, listener)
456471
}
@@ -524,6 +539,7 @@ export function createConfigClient(bridge: DeepchatBridge = getDeepchatBridge())
524539
setDefaultSystemPromptId,
525540
getAcpEnabled,
526541
getAcpAgents,
542+
listAgents,
527543
resolveDeepChatAgentConfig,
528544
getAgentMcpSelections,
529545
getAcpSharedMcpSelections,

0 commit comments

Comments
 (0)