v2-refactor-temp/docs/ai/ai-service-cluster.md
| File | LOC | Role |
|---|---|---|
src/main/ai/AiService.ts | 641 | Lifecycle service; IPC handler registration; non-stream entry points |
src/main/ai/types/requests.ts | 83 | AiBaseRequest, AiStreamRequest, AiTransportOptions, ListModelsRequest |
src/main/ai/types/merged.ts | 102 | AppProviderSettingsMap extension type merging |
src/main/ai/types/providerConfig.ts | 45 | ProviderConfig, ProviderCapabilities, CompletionsResult |
src/main/ai/types/index.ts | — | Re-export barrel (providerConfig + merged) |
| Tests | __tests__/AiService.test.ts (114) | Lifecycle + IPC handler smoke tests |
AiService is the lifecycle-owned IPC owner for the Ai_* channel
namespace. It is intentionally thin — it routes IPC calls into the
shared building blocks (Agent, buildAgentParams, dispatchStreamRequest,
translateService) and is not where business logic lives. Adding a new
LLM-driven IPC entry should be one IPC line in registerIpcHandlers()
plus a method.
| Channel | Mode | Handler |
|---|---|---|
Ai_GenerateText | ipcHandle | generateText(request) — non-streaming |
Ai_CheckModel | ipcHandle | checkModel(request, timeout?) — health probe |
Ai_EmbedMany | ipcHandle | embedMany(request) |
Ai_GenerateImage | ipcOn (MessagePort) | port-based abort, no main-side registry |
Ai_ListModels | ipcHandle | listModels(request) |
Ai_Translate_Open | ipcHandle | translateService.translate(request) — see translate-on-main.md |
Ai_ToolApproval_Respond | ipcHandle | applies decision, dispatches continue-conversation when all decided |
Ai_Stream_Open / Ai_Stream_Attach / Ai_Stream_Abort | ipcHandle | proxied to AiStreamManager (the manager registers these in its own lifecycle) |
Ai_EstimateTokens | ipcHandle | thin forwarder to the token-estimator-p0 pure module |
@Injectable()
@ServicePhase(Phase.WhenReady)
@DependsOn(['McpService', 'AiStreamManager'])
export class AiService extends BaseService {
protected async onInit(): Promise<void> {
registerBuiltinTools()
this.registerIpcHandlers()
}
protected async onStop(): Promise<void> {
toolApprovalRegistry.clear('ai-service-stop')
}
}
@DependsOn(['McpService', 'AiStreamManager']) — explicit
because some methods read from AiStreamManager (e.g. continue
dispatch after approval). The manager is in the same phase; container
resolves the order.onInit — registerBuiltinTools() registers
the built-in tools on the singleton.canUseTool promises
are rejected so they don't hang across a service restart.Ai_GenerateImage uses MessagePortThe image generation channel uses MessagePort instead of ipcHandle
so the renderer can drive abort without a main-side request registry.
Per-call MessageChannel; port2 transferred; renderer posts
{ type: 'abort' }; main sends one terminal result / error and
closes.
This is the only IPC handler in the service that uses ports — the
pattern lives in src/preload/invokeWithAbort.ts and is referenced in
the Ai_GenerateImage handler comments.
Ai_ToolApproval_RespondThe handler resolves an approval-requested ToolUIPart to
approval-approved / approval-denied:
applyApprovalDecisions(beforeParts, [decision]).canUseTool promise (via
toolApprovalRegistry) or dispatches a synthetic
continue-conversation through dispatchStreamRequest.See Tool Approval for the design rationale.
AiRequestOptions vs AiTransportOptionsAiTransportOptions — IPC-serialisable; this is what renderer
payloads use.AiRequestOptions — extends with AbortSignal; only in-process
callers can attach (e.g. AiStreamManager.runExecutionLoop).AsInProcess<T> widens a request type's requestOptions to accept
the in-process shape. Used on AiService.* method signatures so the
type system rejects the renderer trying to pass a signal across IPC.
Ad-hoc one-shot streams (translate, summarisation, model probes) do not
go through AiService. Callers invoke AiStreamManager.streamPrompt(...)
directly (e.g. translateService.ts) with a synthetic topicId and their own
WebContentsListener, using promptStreamLifecycle (no status broadcast, no
grace period). See Stream Manager.
types/requests.ts — AiBaseRequest, AiStreamRequest,
ListModelsRequest. All transport types are flat (no nested
optionality), serialisable.types/merged.ts — AppProviderSettingsMap merges core SDK
CoreProviderSettingsMap with Cherry's app-level extensions
(claude-code, aihubmix, newapi). Provides the AppProviderId union
via StringKeys<...>.Ai_* channels are the only IPC channels AiService owns. The
AiStreamManager owns its own three stream channels.AiTransportOptions —
signal injection happens only on in-process callers.Ai_GenerateImage is the only port-based handler; if a new abort-
capable handler is added, it should follow the same pattern (not
add a main-side abort registry).__tests__/AiService.test.ts (114) — lifecycle smoke + IPC
registration + ToolApproval handler edge cases.Ai_ToolApproval_Respond handler's "all decided?" check assumes
the approval-requested parts live on the anchor message. If
approvals ever land on non-anchor parts we'll revisit.