lobehub/apps/desktop
Arvin Xu 2298ad8ce1
chore(heterogeneous-agent): integrate heterogeneous agents with claude code (#13754)
* ♻️ refactor(acp): move agent provider to agencyConfig + restore creation entry

- Move AgentProviderConfig from chatConfig to agencyConfig.heterogeneousProvider
- Rename type from 'acp' to 'claudecode' for clarity
- Restore Claude Code agent creation entry in sidebar + menu
- Prioritize heterogeneousProvider check over gateway mode in execution flow
- Remove ACP settings from AgentChat form (provider is set at creation time)
- Add getAgencyConfigById selector for cleaner access
- Use existing agent workingDirectory instead of duplicating in provider config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat(acp): defer terminal events + extract model/usage per turn

Three improvements to ACP stream handling:

1. Defer agent_runtime_end/error: Previously the adapter emitted terminal
   events from result.type directly into the Gateway handler. The handler
   immediately fires fetchAndReplaceMessages which reads stale DB state
   (before we persist final content/tools). Fix: intercept terminal events
   in the executor's event loop and forward them only AFTER content +
   metadata has been written to DB.

2. Extract model/usage per assistant event: Claude Code sets model name
   and token usage on every assistant event. Adapter now emits a
   'step_complete' event with phase='turn_metadata' carrying these.
   Executor accumulates input/output/cache tokens across turns and
   persists them onto the assistant message (model + metadata.totalTokens).

3. Missing final text fix: The accumulated assistant text was being
   written AFTER agent_runtime_end triggered fetchAndReplaceMessages,
   so the UI rendered stale (empty) content. Deferred terminals solve this.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

🐛 fix(acp): eliminate orphan-tool warning flicker during streaming

Root cause:
LobeHub's conversation-flow parser (collectToolMessages) filters tool
messages by matching `tool_call_id` against `assistant.tools[].id`. The
previous flow created tool messages FIRST, then updated assistant.tools[],
which opened a brief window where the UI saw tool messages that had no
matching entry in the parent's tools array — rendering them as "orphan"
with a scary "请删除" warning to the user.

Fix:
Reorder persistNewToolCalls into three phases:
  1. Pre-register tool entries in assistant.tools[] (id only, no result_msg_id)
  2. Create the tool messages in DB (tool_call_id matches pre-registered ids)
  3. Back-fill result_msg_id and re-write assistant.tools[]

Between phase 1 and phase 3 the UI always sees consistent state: every
tool message in DB has a matching entry in the parent's tools array.

Verified: orphan count stays at 0 across all sampled timepoints during
streaming (vs 1+ before fix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

🐛 fix(acp): dedupe tool_use + capture tool_result + persist result_msg_id

Three critical fixes to ACP tool-call handling, discovered via live testing:

1. **tool_use dedupe** — Claude Code stream-json previously produced 15+
   duplicate tool messages per tool_call_id. The adapter now tracks emitted
   ids so each tool_use → exactly one tool message.

2. **tool_result content capture** — tool_result blocks live in
   `type: 'user'` events in Claude Code's stream-json, not in assistant
   events. The adapter now handles the 'user' event type and emits a new
   `tool_result` HeterogeneousAgentEvent which the executor consumes to
   call messageService.updateToolMessage() with the actual result content.
   Previously all tool messages had empty content.

3. **result_msg_id on assistant.tools[]** — LobeHub's parse() step links
   tool messages to their parent assistant turn via tools[].result_msg_id.
   Without it, the UI renders orphan-message warnings. The executor now
   captures the tool message id returned by messageService.createMessage
   and writes it back into the assistant.tools[] JSONB.

Also adds vitest config + 9 unit tests for the adapter covering lifecycle,
content mapping, and tool_result handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat(acp): integrate external AI agents via ACP protocol

Adds support for connecting external AI agents (Claude Code and future
agents like Codex, Kimi CLI) into LobeHub Desktop via a new heterogeneous
agent layer that adapts agent-specific protocols to the unified Gateway
event stream.

Architecture:
- New @lobechat/heterogeneous-agents package: pluggable adapters that
  convert agent-specific outputs to AgentStreamEvent
- AcpCtr (Electron main): agent-agnostic process manager with CLI
  presets registry, broadcasts raw stdout lines to renderer
- acpExecutor (renderer): subscribes to broadcasts, runs events through
  adapter, feeds into existing createGatewayEventHandler
- Tool call persistence: creates role='tool' messages via messageService
  before emitting tool_start/tool_end to the handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename acpExecutor to heterogeneousAgentExecutor

- Rename file acpExecutor.ts → heterogeneousAgentExecutor.ts
- Rename ACPExecutorParams → HeterogeneousAgentExecutorParams
- Rename executeACPAgent → executeHeterogeneousAgent
- Change operation type from execAgentRuntime to execHeterogeneousAgent
- Change operation label to "Heterogeneous Agent Execution"
- Change error type from ACPError to HeterogeneousAgentError
- Rename acpData/acpContext variables to heteroData/heteroContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename AcpCtr and acp service to heterogeneousAgent

Desktop side:
- AcpCtr.ts → HeterogeneousAgentCtr.ts
- groupName 'acp' → 'heterogeneousAgent'
- IPC channels: acpRawLine → heteroAgentRawLine, etc.

Renderer side:
- services/electron/acp.ts → heterogeneousAgent.ts
- ACPService → HeterogeneousAgentService
- acpService → heterogeneousAgentService
- Update all IPC channel references in executor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: switch CC permission mode to bypassPermissions

Use bypassPermissions to allow Bash and other tool execution.
Previously acceptEdits only allowed file edits, causing Bash tool
calls to fail during CC execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: don't fallback activeAgentId to empty string in AgentIdSync

Empty string '' causes chat store to have a truthy but invalid
activeAgentId, breaking message routing. Pass undefined instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use AI_RUNTIME_OPERATION_TYPES for loading and cancel states

stopGenerateMessage and cancelOperation were hardcoding
['execAgentRuntime', 'execServerAgentRuntime'], missing
execHeterogeneousAgent. This caused:
- CC execution couldn't be cancelled via stop button
- isAborting flag wasn't set for heterogeneous agent operations

Now uses AI_RUNTIME_OPERATION_TYPES constant everywhere to ensure
all AI runtime operation types are handled consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: split multi-step CC execution into separate assistant messages

Claude Code's multi-turn execution (thinking → tool → final text) was
accumulating everything onto a single assistant message, causing the
final text response to appear inside the tool call message.

Changes:
- ClaudeCodeAdapter: detect message.id changes and emit stream_end +
  stream_start with newStep flag at step boundaries
- heterogeneousAgentExecutor: on newStep stream_start, persist previous
  step's content, create a new assistant message, reset accumulators,
  and forward the new message ID to the gateway handler

This ensures each LLM turn gets its own assistant message, matching
how Gateway mode handles multi-step agent execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fix multi-step CC execution and add DB persistence tests

Adapter fixes:
- Fix false step boundary on first assistant after init (ghost empty message)

Executor fixes:
- Fix parentId chain: new-step assistant points to last tool message
- Fix content contamination: sync snapshot of content accumulators on step boundary
- Fix type errors (import path, ChatToolPayload casts, sessionId guard)

Tests:
- Add ClaudeCodeAdapter unit tests (multi-step, usage, flush, edge cases)
- Add ClaudeCodeAdapter E2E test (full multi-step session simulation)
- Add registry tests
- Add executor DB persistence tests covering:
  - Tool 3-phase write (pre-register → create → backfill)
  - Tool result content + error persistence
  - Multi-step parentId chain (assistant → tool → assistant)
  - Final content/reasoning/model/usage writes
  - Sync snapshot preventing cross-step contamination
  - Error handling with partial content persistence
  - Full multi-step E2E (Read → Write → text)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: add orphan tool regression tests and debug trace

- Add orphan tool regression tests for multi-turn tool execution
- Add __HETERO_AGENT_TRACE debug instrumentation for event flow capture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: support image attachments in CC via stream-json stdin

- Main process downloads files by ID from cloud (GET {domain}/f/{fileId})
- Local disk cache at lobehub-storage/heteroAgent/files/ (by fileId)
- When fileIds present, switches to --input-format stream-json + stdin pipe
- Constructs user message with text + image content blocks (base64)
- Pass fileIds through executor → service → IPC → controller

Closes LOBE-7254

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: pass imageList instead of fileIds for CC vision support

- Use imageList (with url) instead of fileIds — Main downloads from URL directly
- Cache by image id at lobehub-storage/heteroAgent/files/
- Only images (not arbitrary files) are sent to CC via stream-json stdin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: read imageList from persisted DB message instead of chatUploadFileList

chatUploadFileList is cleared after sendMessageInServer, so tempImages
was empty by the time the executor ran. Now reads imageList from the
persisted user message in heteroData.messages instead.

Also removes debug console.log/console.error statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update i18n

* 🐛 fix: prevent orphan tool UI by deferring handler events during step transition

Root cause: when a CC step boundary occurs, the adapter produces
[stream_end, stream_start(newStep), stream_chunk(tools_calling)] in one batch.
The executor deferred stream_start via persistQueue but forwarded stream_chunk
synchronously — handler received tools_calling BEFORE stream_start, dispatching
tools to the OLD assistant message → UI showed orphan tool warning.

Fix: add pendingStepTransition flag that defers ALL handler-bound events through
persistQueue until stream_start is forwarded, guaranteeing correct event ordering.

Also adds:
- Minimal regression test in gatewayEventHandler confirming correct ordering
- Multi-tool per turn regression test from real LOBE-7240 trace
- Data-driven regression replaying 133 real CC events from regression.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add lab toggle for heterogeneous agent (Claude Code)

- Add enableHeterogeneousAgent to UserLabSchema + defaults (off by default)
- Add selector + settings UI toggle (desktop only)
- Gate "Claude Code Agent" sidebar menu item behind the lab setting
- Remove regression.json (no longer needed)
- Add i18n keys for the lab feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: gate heterogeneous agent execution behind isDesktop check

Without this, web users with an agent that has heterogeneousProvider
config would hit the CC execution path and fail (no Electron IPC).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename tool identifier from acp-agent to claude-code

Also update operation label to "External agent running".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add CLI agent detectors for system tools settings

Detect agentic coding CLIs installed on the system:
- Claude Code, Codex, Gemini CLI, Qwen Code, Kimi CLI, Aider
- Uses validated detection (which + --version keyword matching)
- New "CLI Agents" category in System Tools settings
- i18n for en-US and zh-CN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fix token usage over-counting in CC execution

Two bugs fixed:

1. Adapter: same message.id emitted duplicate step_complete(turn_metadata)
   for each content block (thinking/text/tool_use) — all carry identical
   usage. Now deduped by message.id, only emits once per turn.

2. Executor: CC result event contains authoritative session-wide usage
   totals but was ignored. Now adapter emits step_complete(result_usage)
   from the result event, executor uses it to override accumulated values.

Fixes LOBE-7261

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: gitignore cc-stream.json and .heterogeneous-tracing/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: untrack .heerogeneous-tracing/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: wire CC session resume for multi-turn conversations

Reads `ccSessionId` from topic metadata and passes it as `resumeSessionId`
into the heterogeneous-agent executor, which forwards it into the Electron
main-process controller. `sendPrompt` then appends `--resume <id>` so the
next turn continues the same Claude Code session instead of starting fresh.
After each run, the CC init-event session_id (captured by the adapter) is
persisted back onto the topic so the chain survives page reloads.

Also stops killing the session in `finally` — it needs to stay alive for
subsequent turns; cleanup happens on topic deletion or app quit.

* 🐛 fix: record cache token breakdown in CC execution metadata

The prior token-usage fix only wrote totals — `inputCachedTokens`,
`inputWriteCacheTokens` and `inputCacheMissTokens` were dropped, so the
pricing card rendered zero cached/write-cache tokens even though CC had
reported them. Map the accumulated Anthropic-shape usage to the same
breakdown the anthropic usage converter emits, so CC turns display
consistently with Gateway turns.

Refs LOBE-7261

* ♻️ refactor: write CC usage under metadata.usage instead of flat fields

Flat `inputCachedTokens / totalInputTokens / ...` on `MessageMetadata` are
the legacy shape; new code should put usage under `metadata.usage`. Move
the CC executor to the nested shape so it matches the convention the rest
of the runtime is migrating to.

Refs LOBE-7261

* ♻️ refactor(types): mark flat usage fields on MessageMetadata as deprecated

Stop extending `ModelUsage` and redeclare each token field inline with a
`@deprecated` JSDoc pointing to `metadata.usage` (nested). Existing readers
still type-check, but IDEs now surface the deprecation so writers migrate
to the nested shape.

* ♻️ refactor(types): mark flat performance fields on MessageMetadata as deprecated

Stop extending `ModelPerformance` and redeclare `duration` / `latency` /
`tps` / `ttft` inline with `@deprecated`, pointing at `metadata.performance`.
Mirrors the same treatment just done for the token usage fields.

*  feat: CC agent gets claude avatar + lands on chat page directly

Skip the shared createAgent hook's /profile redirect for the Claude Code
variant — its config is fixed so the profile editor would be noise — and
preseed the Claude avatar from @lobehub/icons-static-avatar so new CC
agents aren't blank.

* 🐛 fix(conversation-flow): read usage/performance from nested metadata

`splitMetadata` only scraped the legacy flat token/perf fields, so messages
written under the new canonical shape (`metadata.usage`, `metadata.performance`)
never populated `UIChatMessage.usage` and the Extras panel rendered blank.

- Prefer nested `metadata.usage` / `metadata.performance` when present; keep
  flat scraping as fallback for pre-migration rows.
- Add `usage` / `performance` to FlatListBuilder's filter sets so the nested
  blobs don't leak into `otherMetadata`.
- Drop the stale `usage! || metadata` fallback in the Assistant / CouncilMember
  Extra renders — with splitMetadata fixed, `item.usage` is always populated
  when usage data exists, and passing raw metadata as ModelUsage is wrong now
  that the flat fields are gone.

* 🐛 fix: skip stores.reset on initial dataSyncConfig hydration

`useDataSyncConfig`'s SWR onSuccess called `refreshUserData` (which runs
`stores.reset()`) whenever the freshly-fetched config didn't deep-equal the
hard-coded initial `{ storageMode: 'cloud' }` — which happens on every
first load. The reset would wipe `chat.activeAgentId` just after
`AgentIdSync` set it from the URL, and because `AgentIdSync`'s sync
effects are keyed on `params.aid` (which hasn't changed), they never re-fire
to restore it. Result: topic SWR saw `activeAgentId === ''`, treated the
container as invalid, and left the sidebar stuck on the loading skeleton.

Gate the reset on `isInitRemoteServerConfig` so it only runs when the user
actually switches sync modes, not on the first hydration.

*  feat(claude-code): wire Inspector layer for CC tool calls

Mirrors local-system: each CC tool now has an inspector rendered above the
tool-call output instead of an opaque default row.

- `Inspector.tsx` — registry that passes the CC tool name itself as the
  shared factories' `translationKey`. react-i18next's missing-key fallback
  surfaces the literal name (Bash / Edit / Glob / Grep / Read / Write), so
  we don't add CC-specific entries to the plugin locale.
- `ReadInspector.tsx` / `WriteInspector.tsx` — thin adapters that map
  Anthropic-native args (`file_path` / `offset` / `limit`) onto the shared
  inspectors' shape (`path` / `startLine` / `endLine`), so shared stays
  pure. Bash / Edit / Glob / Grep reuse shared factories directly.
- Register `ClaudeCodeInspectors` under `claude-code` in the builtin-tools
  inspector dispatch.

Also drops the redundant `Render/Bash/index.tsx` wrapper and pipes the
shared `RunCommandRender` straight into the registry.

* ♻️ refactor: use agentSelectors.isCurrentAgentHeterogeneous

Two callsites (ConversationArea / useActionsBarConfig) were reaching into
`currentAgentConfig(...)?.agencyConfig?.heterogeneousProvider` inline.
Switch them to the existing `isCurrentAgentHeterogeneous` selector so the
predicate lives in one place.

* update

* ♻️ refactor: drop no-op useCallback wrapper in AgentChat form

`handleFinish` just called `updateConfig(values)` with no extra logic; the
zustand action is already a stable reference so the wrapper added no
memoization value. Leftover from the ACP refactor (930ba41fe3) where the
handler once did more work — hand the action straight to `onFinish`.

* update

*  revert: roll back conversation-flow nested-shape reads

Unwind the `splitMetadata` nested-preference + `FlatListBuilder` filter
additions from 306fd6561f. The nested `metadata.usage` / `metadata.performance`
promotion now happens in `parse.ts` (and a `?? metadata?.usage` fallback at
the UI callsites), so conversation-flow's transformer layer goes back to
its original flat-field-only behavior.

* update

* 🐛 fix(cc): wire Stop to cancel the external Claude Code process

Previously hitting Stop only flipped the `execHeterogeneousAgent` operation
to `cancelled` in the store — the spawned `claude -p` process kept
running and kept streaming/persisting output for the user. The op's abort
signal had no listeners and no `onCancelHandler` was registered.

- On session start, register an `onCancelHandler` that calls
  `heterogeneousAgentService.cancelSession(sessionId)` (SIGINT to the CLI).
- Read the op's `abortController.signal` and short-circuit `onRawLine` so
  late events the CLI emits between SIGINT and exit don't leak into DB
  writes.
- Skip the error-event forward in `onError` / the outer catch when the
  abort came from the user, so the UI doesn't surface a misleading error
  toast on top of the already-cancelled operation.

Verified end-to-end: prompt that runs a long sequence of Reads → click
Stop → `claude -p` process is gone within 2s, op status = cancelled, no
error message written to the conversation.

*  feat(sidebar): mark heterogeneous agents with an "External" tag

Pipes the agent's `agencyConfig.heterogeneousProvider.type` through the
sidebar data flow and renders a `<Tag>` next to the title for any agent
driven by an external CLI runtime (Claude Code today, more later). Mirrors
the group-member External pattern so future provider types just need a
label swap — the field is a string, not a boolean.

- `SidebarAgentItem.heterogeneousType?: string | null` on the shared type
- `HomeRepository.getSidebarAgentList` selects `agents.agencyConfig` and
  derives the field via `cleanObject`
- `AgentItem` shows `<Tag>{t('group.profile.external')}</Tag>` when the
  field is present

Verified client-side by injecting `heterogeneousType: 'claudecode'` into
a sidebar item at runtime — the "外部" tag renders next to the title in
the zh-CN locale.

* ♻️ refactor(i18n): dedicated key for the sidebar external-agent tag

Instead of reusing `group.profile.external` (which is about group members
that are user-linked rather than virtual), add `agentSidebar.externalTag`
specifically for the heterogeneous-runtime tag. Keeps the two concepts
separate so we can swap this one to "Claude Code" / provider-specific
labels later without touching the group UI copy.

Remember to run `pnpm i18n` before the PR so the remaining locales pick
up the new key.

* 🐛 fix: clear remaining CI type errors

Three small fixes so `tsgo --noEmit` exits clean:

- `AgentIdSync`: `useChatStoreUpdater` is typed off the chat-store key, whose
  `activeAgentId` is `string` (initial ''). Coerce the optional URL param to
  `''` so the store key type matches; `createStoreUpdater` still skips the
  setState when the value is undefined-ish.
- `heterogeneousAgentExecutor.test.ts`: `scope: 'session'` isn't a valid
  `MessageMapScope` (the union dropped that variant); switch the fixture to
  `'main'`, which is the correct scope for agent main conversations.
- Same test file: `Array.at(-1)` is `T | undefined`; non-null assert since
  the preceding calls guarantee the slot is populated.

* 🐛 fix: loosen createStoreUpdater signature to accept nullable values

Upstream `createStoreUpdater` types `value` as exactly `T[Key]`, so any
call site feeding an optional source (URL param, selector that may return
undefined) fails type-check — even though the runtime already guards
`typeof value !== 'undefined'` and no-ops in that case.

Wrap it once in `store/utils/createStoreUpdater.ts` with a `T[Key] | null
| undefined` value type so callers can pass `params.aid` directly, instead
of the lossy `?? ''` fallback the previous commit used (which would have
written an empty-string sentinel into the chat store).

Swap the import in `AgentIdSync.tsx`.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 19:33:39 +08:00
..
build feat: rebrand stable app icon 2026-02-11 12:58:44 +08:00
resources 🔨 chore(i18n): sync locale files across desktop and web (#12887) 2026-03-10 19:23:47 +08:00
scripts 🌐 chore: translate non-English comments to English in desktop i18nWorkflow (#13604) 2026-04-07 16:51:56 +08:00
src chore(heterogeneous-agent): integrate heterogeneous agents with claude code (#13754) 2026-04-17 19:33:39 +08:00
stubs ♻️ refactor(desktop): consolidate global shortcuts (LOBE-7181) (#13880) 2026-04-17 00:32:05 +08:00
.gitignore feat(agent-browser): add browser automation skill and tool detection (#12858) 2026-03-10 16:13:33 +08:00
.i18nrc.js 🐛 fix(desktop): add auth required modal and improve error handling (#11574) 2026-01-18 18:55:18 +08:00
.npmrc feat: remove Clerk authentication code (#11711) 2026-01-23 14:41:22 +08:00
.prettierignore feat: refactor desktop implement with brand new 2.0 2025-12-24 12:54:35 +08:00
.remarkrc.mjs 🔧 chore: update eslint v2 configuration and suppressions (#12133) 2026-02-11 13:04:48 +08:00
.stylelintignore feat: refactor desktop implement with brand new 2.0 2025-12-24 12:54:35 +08:00
dev-app-update.yml feat(desktop): unified update channel switching with S3 distribution (#12644) 2026-03-05 15:15:03 +08:00
Development.md 📝 docs(desktop): update Development.md to reflect current project structure [skip ci] (#12581) 2026-03-02 15:36:04 +08:00
electron-builder.mjs 🐛 fix: fix minify cli (#13888) 2026-04-16 18:39:18 +08:00
electron.vite.config.ts feat(desktop): embed CLI in app and PATH install (#13669) 2026-04-09 00:53:49 +08:00
index.html 🐛 fix(desktop): use stored locale from URL parameter instead of syste… (#13620) 2026-04-07 22:58:09 +08:00
native-deps.config.mjs 🐛 fix(desktop): remove electron-liquid-glass to fix click event blocking (#13070) 2026-03-17 22:56:29 +08:00
package.json 🐛 fix: fix minify cli (#13888) 2026-04-16 18:39:18 +08:00
pnpm-workspace.yaml ♻️ refactor(desktop): consolidate global shortcuts (LOBE-7181) (#13880) 2026-04-17 00:32:05 +08:00
prettier.config.mjs 🔧 chore: update eslint v2 configuration and suppressions (#12133) 2026-02-11 13:04:48 +08:00
README.md 📝 docs: update documents (#12982) 2026-03-14 22:06:09 +08:00
README.zh-CN.md 📝 docs: update documents (#12982) 2026-03-14 22:06:09 +08:00
stylelint.config.mjs 🔧 chore: update eslint v2 configuration and suppressions (#12133) 2026-02-11 13:04:48 +08:00
tsconfig.json feat(electron): enhance native module handling and improve desktop features (#11867) 2026-01-27 01:30:08 +08:00
vitest.config.mts ♻️ refactor(cli): extract shared @lobechat/local-file-shell package (#12865) 2026-03-11 00:04:22 +08:00

🤯 LobeHub Desktop Application

LobeHub Desktop is a cross-platform desktop application for LobeHub, built with Electron, providing a more native desktop experience and functionality.

Features

  • 🌍 Cross-platform Support: Supports macOS (Intel/Apple Silicon), Windows, and Linux systems
  • 🔄 Auto Updates: Built-in update mechanism ensures you always have the latest version
  • 🌐 Multi-language Support: Complete i18n support for 18+ languages with lazy loading
  • 🎨 Native Integration: Deep OS integration with native menus, shortcuts, and notifications
  • 🔒 Secure & Reliable: macOS notarized, encrypted token storage, secure OAuth flow
  • 📦 Multiple Release Channels: Stable, beta, and nightly build versions
  • Advanced Window Management: Multi-window architecture with theme synchronization
  • 🔗 Remote Server Sync: Secure data synchronization with remote LobeHub instances
  • 🎯 Developer Tools: Built-in development panel and comprehensive debugging tools

🚀 Development Setup

Prerequisites

  • Node.js 22+
  • pnpm 10+
  • Electron compatible development environment

Quick Start

# Install dependencies
pnpm install-isolated

# Start development server
pnpm dev

# Type checking
pnpm type-check

# Run tests
pnpm test

Environment Configuration

Copy .env.desktop to .env and configure as needed:

cp .env.desktop .env

[!WARNING] Backup your .env file before making changes to avoid losing configurations.

Build Commands

Command Description
pnpm build:main Build main/preload (dist output only)
pnpm package:mac Package for macOS (Intel + Apple Silicon)
pnpm package:win Package for Windows
pnpm package:linux Package for Linux
pnpm package:local Local packaging build (no ASAR)
pnpm package:local:reuse Local packaging build reusing existing dist

Development Workflow

# 1. Development
pnpm dev # Start with hot reload

# 2. Code Quality
pnpm lint       # ESLint checking
pnpm format     # Prettier formatting
pnpm type-check # TypeScript validation

# 3. Testing
pnpm test # Run Vitest tests

# 4. Build & Package
pnpm build:main    # Production build (dist only)
pnpm package:local # Local testing package

🎯 Release Channels

Channel Description Stability Auto-Updates
Stable Thoroughly tested releases 🟢 High Yes
Beta Pre-release with new features 🟡 Medium Yes
Nightly Daily builds with latest changes 🟠 Low Yes

🛠 Technology Stack

Core Framework

  • Electron 37.1.0 - Cross-platform desktop framework
  • Node.js 22+ - Backend runtime
  • TypeScript 5.7+ - Type-safe development
  • Vite 6.2+ - Build tooling

Architecture & Patterns

  • Dependency Injection - IoC container with decorator-based registration
  • Event-Driven Architecture - IPC communication between processes
  • Module Federation - Dynamic controller and service loading
  • Observer Pattern - State management and UI synchronization

Development Tools

  • Vitest - Unit testing framework
  • ESLint - Code linting
  • Prettier - Code formatting
  • electron-builder - Application packaging
  • electron-updater - Auto-update mechanism

Security & Storage

  • Electron Safe Storage - Encrypted token storage
  • OAuth 2.0 + PKCE - Secure authentication flow
  • electron-store - Persistent configuration
  • Custom Protocol Handler - Secure callback handling

🏗 Architecture

The desktop application uses a sophisticated dependency injection and event-driven architecture:

📁 Core Structure

src/main/core/
├── App.ts                    # 🎯 Main application orchestrator
├── IoCContainer.ts           # 🔌 Dependency injection container
├── window/                   # 🪟 Window management modules
│   ├── WindowThemeManager.ts     # 🎨 Theme synchronization
│   ├── WindowPositionManager.ts  # 📐 Position persistence
│   ├── WindowErrorHandler.ts     # ⚠️  Error boundaries
│   └── WindowConfigBuilder.ts    # ⚙️  Configuration builder
├── browser/                  # 🌐 Browser management modules
│   ├── Browser.ts               # 🪟 Individual window instances
│   └── BrowserManager.ts        # 👥 Multi-window coordinator
├── ui/                       # 🎨 UI system modules
│   ├── Tray.ts                  # 📍 System tray integration
│   ├── TrayManager.ts           # 🔧 Tray management
│   ├── MenuManager.ts           # 📋 Native menu system
│   └── ShortcutManager.ts       # ⌨️  Global shortcuts
└── infrastructure/           # 🔧 Infrastructure services
    ├── StoreManager.ts          # 💾 Configuration storage
    ├── I18nManager.ts           # 🌍 Internationalization
    ├── UpdaterManager.ts        # 📦 Auto-update system
    └── StaticFileServerManager.ts # 🗂️ Local file serving

🔄 Application Lifecycle

The App.ts class orchestrates the entire application lifecycle through key phases:

1. 🚀 Initialization Phase

  • System Information Logging - Captures OS, CPU, RAM, and locale details
  • Store Manager Setup - Initializes persistent configuration storage
  • Dynamic Module Loading - Auto-discovers controllers and services via glob imports
  • IPC Event Registration - Sets up inter-process communication channels

2. 🏃 Bootstrap Phase

  • Single Instance Check - Ensures only one application instance runs
  • IPC Server Launch - Starts the communication server
  • Core Manager Initialization - Sequential initialization of all managers:
    • 🌍 I18n for internationalization
    • 📋 Menu system for native menus
    • 🗂️ Static file server for local assets
    • ⌨️ Global shortcuts registration
    • 🪟 Browser window management
    • 📍 System tray (Windows only)
    • 📦 Auto-updater system

🔧 Core Components Deep Dive

🌐 Browser Management System

  • Multi-Window Architecture - Supports chat, settings, and devtools windows
  • Window State Management - Handles positioning, theming, and lifecycle
  • WebContents Mapping - Bidirectional mapping between WebContents and identifiers
  • Event Broadcasting - Centralized event distribution to all or specific windows

🔌 Dependency Injection & Event System

  • IoC Container - WeakMap-based container for decorated controller methods
  • Typed IPC Decorators - @IpcMethod wires controller methods into type-safe channels
  • Automatic Event Mapping - Events registered during controller loading
  • Service Locator - Type-safe service and controller retrieval
🧠 Type-Safe IPC Flow
  • Async Context Propagation - src/main/utils/ipc/base.ts captures the IpcContext with AsyncLocalStorage, so controller logic can call getIpcContext() anywhere inside an IPC handler without explicitly threading arguments.
  • Service Constructors Registry - src/main/controllers/registry.ts exports controllerIpcConstructors and DesktopIpcServices, enabling automatic typing of renderer IPC proxies.
  • Renderer Proxy Helper - src/utils/electron/ipc.ts exposes ensureElectronIpc() which lazily builds a proxy on top of window.electronAPI.invoke, giving React/Next.js code a type-safe API surface without exposing raw proxies in preload.
  • Shared Typings Package - apps/desktop/src/main/exports.d.ts augments @lobechat/electron-client-ipc so every package can consume DesktopIpcServices without importing desktop business code directly.

🪟 Window Management

  • Theme-Aware Windows - Automatic adaptation to system dark/light mode
  • Platform-Specific Styling - Windows title bar and overlay customization
  • Position Persistence - Save and restore window positions across sessions
  • Error Boundaries - Centralized error handling for window operations

🔧 Infrastructure Services

🌍 I18n Manager
  • 18+ Language Support with lazy loading and namespace organization
  • System Integration with Electron's locale detection
  • Dynamic UI Refresh on language changes
  • Resource Management with efficient loading strategies
📦 Update Manager
  • Multi-Channel Support (stable, beta, nightly) with configurable intervals
  • Background Downloads with progress tracking and user notifications
  • Rollback Protection with error handling and recovery mechanisms
  • Channel Management with automatic channel switching
💾 Store Manager
  • Type-Safe Storage using electron-store with TypeScript interfaces
  • Encrypted Secrets via Electron's Safe Storage API
  • Configuration Validation with default value management
  • File System Integration with automatic directory creation
🗂️ Static File Server
  • Local HTTP Server for serving application assets and user files
  • Security Controls with request filtering and access validation
  • File Management with upload, download, and deletion capabilities
  • Path Resolution with intelligent routing between storage locations

🎨 UI System Integration

  • Global Shortcuts - Platform-aware keyboard shortcut registration with conflict detection
  • System Tray - Native integration with context menus and notifications
  • Native Menus - Platform-specific application and context menus with i18n
  • Theme Synchronization - Automatic theme updates across all UI components

🏛 Controller & Service Architecture

🎮 Controller Pattern

  • Typed IPC Decorators - Controllers extend ControllerModule and expose renderer methods via @IpcMethod
  • IPC Event Handling - Processes events from renderer with decorator-based registration
  • Lifecycle Hooks - beforeAppReady and afterAppReady for initialization phases
  • Type-Safe Communication - Strong typing for all IPC events and responses
  • Error Boundaries - Comprehensive error handling with proper propagation

🔧 Service Pattern

  • Business Logic Encapsulation - Clean separation of concerns
  • Dependency Management - Managed through IoC container
  • Cross-Controller Sharing - Services accessible via service locator pattern
  • Resource Management - Proper initialization and cleanup

🔗 Inter-Process Communication

📡 IPC System Features

  • Bidirectional Communication - Main↔Renderer and Main↔Next.js server
  • Type-Safe Events - TypeScript interfaces for all event parameters
  • Context Awareness - Events include sender context for window-specific operations
  • Error Propagation - Centralized error handling with proper status codes
🧩 Renderer IPC Helper

Renderer code uses a lightweight proxy generated at runtime to keep IPC calls type-safe without exposing raw Electron objects through contextBridge. Use the helper exported from src/utils/electron/ipc.ts to access the main-process services:

import { ensureElectronIpc } from '@/utils/electron/ipc';

const ipc = ensureElectronIpc();
await ipc.windows.openSettingsWindow({ tab: 'provider' });

The helper internally builds a proxy on top of window.electronAPI.invoke, so no proxy objects need to be cloned across the preload boundary.

🛡️ Security Features

  • OAuth 2.0 + PKCE - Secure authentication with state parameter validation
  • Encrypted Token Storage - Using Electron's Safe Storage API when available
  • Custom Protocol Handler - Secure callback handling for OAuth flows
  • Request Filtering - Security controls for web requests and external links

🧪 Testing

Test Structure

apps/desktop/src/main/controllers/__tests__/ # Controller unit tests
tests/                                       # Integration tests

Running Tests

pnpm test       # Run all tests
pnpm test:watch # Watch mode
pnpm type-check # Type validation

Test Coverage

  • Controller Tests - IPC event handling validation
  • Service Tests - Business logic verification
  • Integration Tests - End-to-end workflow testing
  • Type Tests - TypeScript interface validation

🔒 Security Features

Authentication & Authorization

  • OAuth 2.0 Flow with PKCE for secure token exchange
  • State Parameter Validation to prevent CSRF attacks
  • Encrypted Token Storage using platform-native secure storage
  • Automatic Token Refresh with fallback to re-authentication

Application Security

  • Code Signing - macOS notarization for enhanced security
  • Sandboxing - Controlled access to system resources
  • CSP Controls - Content Security Policy management
  • Request Filtering - Security controls for external requests

Data Protection

  • Encrypted Configuration - Sensitive data encrypted at rest
  • Secure IPC - Type-safe communication channels
  • Path Validation - Secure file system access controls
  • Network Security - HTTPS enforcement and proxy support

🤝 Contribution

Desktop application development involves complex cross-platform considerations and native integrations. We welcome community contributions to improve functionality, performance, and user experience. You can participate in improvements through:

How to Contribute

  1. Platform Support: Enhance cross-platform compatibility and native integrations
  2. Performance Optimization: Improve application startup time, memory usage, and responsiveness
  3. Feature Development: Add new desktop-specific features and capabilities
  4. Bug Fixes: Fix platform-specific issues and edge cases
  5. Security Improvements: Enhance security measures and authentication flows
  6. UI/UX Enhancements: Improve desktop user interface and experience

Contribution Process

  1. Fork the LobeHub repository
  2. Set up the desktop development environment following our setup guide
  3. Make your changes to the desktop application
  4. Submit a Pull Request describing:
  • Platform compatibility testing results
  • Performance impact analysis
  • Security considerations
  • User experience improvements
  • Breaking changes (if any)

Development Areas

  • Core Architecture: Dependency injection, event system, and lifecycle management
  • Window Management: Multi-window support, theme synchronization, and state persistence
  • IPC Communication: Type-safe inter-process communication between main and renderer
  • Platform Integration: Native menus, shortcuts, notifications, and system tray
  • Security Features: OAuth flows, token encryption, and secure storage
  • Auto-Update System: Multi-channel updates and rollback mechanisms

📚 Additional Resources