Compare commits

..

134 Commits

Author SHA1 Message Date
rdmclin2 48aa0ad245 chore: update neon webSocketConstructor ws 2026-04-21 18:29:07 +08:00
Arvin Xu b02b727261 feat(heterogeneous-agent): support CC subagent rendering (#14001)
*  feat(heterogeneous-agents): preserve CC subagent lineage in adapter

Restores the CC subagent-lineage adapter work that was held back from
#LOBE-7392 until the thread-router backend changes ship. This PR targets
the LOBE-7392 branch so the adapter diff stays isolated from the
thread/UI foundation — GitHub will auto-retarget to canary once
LOBE-7392 merges.

Original scope (unchanged from the held-back commits):
- ToolCallPayload.parentToolCallId carries parent tool_use id downstream
  so consumers can group subagent inner tools under their spawning
  parent.
- claudeCode.ts routes raw.parent_tool_use_id events through
  handleSubagentAssistant so the main-agent step tracker is not advanced
  on subagent message.id changes, usage is not double-counted, and
  subagent text / reasoning are dropped (their final answer flows back
  via the outer tool_result).
- emitToolChunk helper shared by main-agent and subagent paths so new
  suppress-rules live in one place.
- 6 subagent-lineage tests: lineage propagation, no newStep on
  subagent message.id change, no turn_metadata emission, text/reasoning
  drop, main-agent step boundary resumes after subagent, subagent
  tool_result passthrough.

Refs LOBE-7319, LOBE-7260

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

* 💄 style(workflow-collapse): move expand toggle to action slot

Pass the fullscreen toggle as AccordionItem action so the built-in
chevron indicator (same as TopicList) sits inline with the title on
the left, with Maximize2/Minimize2 on the right.

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

*  feat(heterogeneous-agents): route CC Task tool_use to subagent Thread

When a main-agent tool_use spawns a subagent, the executor now sync-
allocates a threadId and creates a Thread, routing subsequent subagent
inner tool_uses (tagged with `parentToolCallId` by the adapter) into
that thread instead of the main assistant's tools[].

The "this tool_use spawns a subagent" decision lives entirely in the
adapter layer via a new `ToolCallPayload.subagentSpawn` descriptor
(`description`, `subagentType`). The CC adapter populates it on every
`Task` tool_use; when Codex (or any other CLI) grows a subtask concept,
its adapter populates the same field and the executor needs zero
changes. The executor never checks `identifier === 'claude-code'` or
`apiName === 'Task'` — it just reacts to the presence of
`subagentSpawn`.

- `ToolCallPayload.subagentSpawn?: { description?, subagentType? }`
  in `packages/heterogeneous-agents/src/types.ts` — adapter-agnostic
  spawn signal, paired with the existing `parentToolCallId` (which
  marks tool_uses BELONGING to a subagent). Together they cover both
  directions of the lineage.
- `claudeCode.ts` stamps `subagentSpawn` on main-agent `Task` tool_uses
  using the already-parsed `block.input` — no redundant JSON.parse.
- `ThreadService.createThread` helper wraps the sync-id TRPC mutation
  shipped in #14000. `generateThreadId()` mirrors the server's
  `idGenerator('threads', 16)` shape (`thd_<16 chars>`) so caller-
  provided ids match the schema pattern.
- `persistNewToolCalls` splits fresh tools into main/subagent groups:
  Phase 1 (pre-register assistant.tools[]) and Phase 3 (backfill
  result_msg_id) run for main tools only. A new Phase 1b creates the
  Thread per `subagentSpawn` — guarded on `context.topicId` (required
  for Thread creation; missing falls back to normal tool rendering).
  Phase 2 writes tool messages for both groups, attaching `threadId`
  to subagent writes. Orphaned subagent events (parent spawn never
  registered) warn + drop instead of leaking into the main timeline.
- `taskThreadMap` lives at executor scope (not on ToolPersistenceState
  which resets per step) so pathological orderings that straddle the
  main-agent step boundary can't lose the parent→thread mapping.

7 new tests: 2 adapter-level (subagentSpawn stamped on Task,
NOT stamped on Read) + 5 executor-level (Thread creation, threadId
propagation onto subagent tool messages, main assistant.tools[]
isolation, orphan drop + warn, topicId-missing fallback).

Refs LOBE-7319, LOBE-7392

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

*  feat(types): persist subagent lineage fields on ChatToolPayload schema

Add `parentToolCallId` and `subagentSpawn` as first-class optional
fields on `ChatToolPayload` + `ChatToolPayloadSchema`, so the adapter-
emitted lineage metadata survives the TRPC `update-message` gate
instead of being silently stripped by zod's default strip behavior.

Reviewer-flagged bug: `UpdateMessageParamsSchema.tools` runs each
payload through `ChatToolPayloadSchema`, which previously only
whitelisted `apiName / arguments / id / identifier / intervention /
result_msg_id / thoughtSignature / type`. Any adapter-level
extension (subagent spawn marker, parent-child pointer) was dropped
before it ever reached the `messages.tools` JSONB column, so lineage
only lived in transient stream events and vanished on the first
`tool_end → fetchAndReplaceMessages`. Downstream consumers that
wanted to key off `tool.subagentSpawn` to render a TaskBlock, or
follow `tool.parentToolCallId` to reconstruct the spawning parent,
had nothing to work with.

- `SubagentSpawnInfo` + `SubagentSpawnInfoSchema` defined in
  `packages/types/src/message/common/tools.ts` as the canonical
  shape. Structurally identical to the same-named type in
  `@lobechat/heterogeneous-agents` (which stays self-contained by
  design) — TypeScript structural typing handles the bridge.
- Both new fields are optional on the interface and the zod schema,
  so existing callers continue to parse unchanged.
- Jsonb column accepts any shape, so no DB migration — the only
  missing piece was the schema gate.

3 new regression tests next to the executor's subagent-thread-routing
suite, asserting `ChatToolPayloadSchema.parse()` preserves both
fields and the same fields survive through `UpdateMessageParamsSchema`
(the actual TRPC gate that was stripping them before).

Refs LOBE-7319

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

* Revert " feat(types): persist subagent lineage fields on ChatToolPayload schema"

This reverts commit 042e48c7338aa8b502bcd6298a2871c758f348af.

* ♻️ refactor(heterogeneous-agents): lift subagent context to event-peer fields

`ToolCallPayload` is "one tool call" — it shouldn't carry stream-level
lineage (parent spawn id, subagent turn id). That info describes the
containing event/chunk and should live as a peer field on the event
`data`, not nested inside each payload.

Event model changes:
- New `SubagentEventContext` + `SubagentSpawnMetadata` types. Events
  originating from a subagent stream (CC Task, future Codex subtask,
  etc.) carry `data.subagent` as a peer field next to `toolsCalling`
  / `toolCallId`. Covers `stream_chunk` (tools_calling), `tool_start`,
  `tool_end`, and `tool_result`.
- `SubagentEventContext.spawnMetadata` appears ONLY on the first event
  for each new parent — lets the executor lazy-create the subagent
  Thread on first sight without needing to know CC-specific argument
  shapes or to re-parse `tool_use.input`. Subsequent events for the
  same parent carry just the lineage ids.
- `ToolCallPayload` is back to its minimal form (`apiName / arguments
  / id / identifier / type`). No `parentToolCallId`, no `subagentSpawn`
  — those were the wrong abstraction level; removing them also sidesteps
  the `ChatToolPayloadSchema` strip-on-persist issue (the fields never
  need to survive DB roundtrip because Thread container persistence
  expresses the lineage).

CC adapter (`claudeCode.ts`):
- `handleSubagentAssistant` emits tools through a shared `emitToolChunk`
  that stamps the `subagent` peer field on the chunk + each tool_start.
  The FIRST subagent chunk for a new parent gets `spawnMetadata` pulled
  from a new adapter-internal `taskArgsById` cache — description /
  prompt / subagentType — announced exactly once via `announcedSpawns`.
- `handleUser` stamps `subagent.parentToolCallId` on `tool_result` +
  `tool_end` when the user event carries `parent_tool_use_id`
  (CC's shape for subagent inner tool_results).
- Main-agent tool_use handling no longer stamps lineage on payloads.

Adapter tests updated — 4 rewrites in the subagent suite:
- assert chunk-level peer fields (not payload-nested lineage)
- assert `spawnMetadata` on first subagent event, absent on subsequent
- assert main-agent tool_uses don't get `subagent` context
- assert subagent `tool_result` + `tool_end` carry the peer

59 adapter tests pass (52 existing + 7 covering the new peer contract).

Refs LOBE-7319, LOBE-7392

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

*  feat(heterogeneous-agents): persist subagent runs as Thread containers

Subagents now materialize as a nested conversation inside a Thread,
shaped identically to the main topic:

    Thread
    ├─ user          (content = Task prompt, threadId=thread.id)
    ├─ assistant#1   (tools[] = subagent turn 1 tool_uses, threadId)
    ├─ tool          (parentId=assistant#1, threadId)
    ├─ assistant#2   (tools[] = subagent turn 2 tool_uses, threadId)
    └─ tool          (parentId=assistant#2, threadId)

Same schema as a main topic, just rooted at a Thread instead of a
Topic. No new persistence shape, no new renderer — the existing
`query({ threadId })` read path reconstructs the subagent's full
conversation when the UI expands the TaskBlock.

Executor changes:
- `ToolPersistenceState` shrinks to `{ payloads, persistedIds }` — the
  `tool_use.id → tool message DB id` map moves to executor scope as
  one global `toolMsgIdByCallId` shared across main + every subagent
  run. `tool_result` lookups don't care which scope created the row.
- `persistNewToolCalls` → renamed `persistToolBatch` and made scope-
  agnostic (takes an optional `threadId` + the global id map). Runs
  the same 3-phase flow (pre-register → create → backfill) whether
  target is main assistant or in-thread subagent assistant.
- New `persistSubagentToolChunk` handles the subagent path: reads the
  adapter's `SubagentEventContext` peer field off the chunk, lazy-
  creates the Thread + user message on the FIRST chunk for each
  parent (using `spawnMetadata`), opens a new in-thread assistant on
  `subagentMessageId` change (same shape as main-agent step
  boundary), then delegates to `persistToolBatch`.
- `SubagentRunState` tracks per-parent Thread id, current in-thread
  assistant, `currentSubagentMessageId`, chain parent, and its own
  `ToolPersistenceState`. Lives at executor scope so subagent events
  straddling a main-agent step boundary keep their mapping.
- Step-boundary parent lookup reads from `toolState.payloads` (not
  the global id map) so main-agent chain doesn't accidentally pick
  up a subagent tool's msg id as the step parent.
- Executor has NO CC-specific knowledge — it never checks
  `identifier`, `apiName`, or parses `tool_use.arguments`. All CC
  quirks live in the adapter; new CLIs (Codex subtask, ...) plug in
  by emitting the same `SubagentEventContext` peer.

Test rewrite — 6 tests under "CC subagent thread-container":
- Task tool_use alone does NOT create a Thread (lazy)
- First subagent event creates Thread + `role:'user'` seeded with
  the Task prompt + first in-thread `role:'assistant'`
- Subagent inner tools persist as `role:'tool'` messages with
  threadId set and parentId chained to the in-thread assistant
- `subagentMessageId` change opens a new in-thread assistant
- Main `assistant.tools[]` carries Task only; subagent inner tools
  appear on the in-thread assistant's `tools[]`
- Missing topicId gracefully skips Thread creation

25 executor tests pass (19 existing + 6 rewritten for new shape).

Refs LOBE-7319, LOBE-7392

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

*  feat(heterogeneous-agents): subagent prompt + closing summary in Thread view

Electron E2E surfaced two gaps in the Thread-container model shipped in
the previous commit:

1. **Subagent user-message content empty.** Real CC emits `Agent` as
   the spawn-tool name for general-purpose subagents (not only `Task`
   as the spec documents). My earlier `taskArgsById` cache keyed off
   `ClaudeCodeApiName.Task` only, so `spawnMetadata.prompt` was
   undefined when the user watched the actual app — the Thread's
   `role:'user'` message landed with empty content and the thread
   view looked like a tool call floating alone.

2. **No closing summary in the Thread.** The adapter dropped subagent
   text/reasoning per an earlier comment claiming the subagent's
   final answer arrives via the outer tool_result. That's true for
   the MAIN timeline (the outer spawn tool's result content = the
   subagent's summary), but the THREAD view is a standalone
   conversation — dropping the subagent's final text left it ending
   on a bare tool call with no assistant conclusion.

Adapter changes (`claudeCode.ts`):
- Rename `taskArgsById` → `mainToolInputsById` and cache EVERY
  main-agent tool_use input (not just `Task`). `emitToolChunk` looks
  up the parent's input by `parent_tool_use_id` on the first subagent
  event and extracts `description` / `prompt` / `subagent_type`
  defensively — any CC spawn-tool variant that shares this input
  shape (`Task`, `Agent`, future ones) gets spawn metadata for free.
- `handleSubagentAssistant` stops filtering `tool_use` only. Text
  and `thinking` blocks now emit as `stream_chunk` events with the
  `subagent` peer field attached — routed to the in-thread assistant,
  NOT the main assistant's accumulators.

Executor changes (`heterogeneousAgentExecutor.ts`):
- `SubagentRunState` gains `accumulatedContent` + `accumulatedReasoning`,
  mirroring main-agent content tracking.
- Extract `ensureSubagentRun` helper so text chunks and tool chunks
  share the Thread / user / assistant lifecycle logic. On turn
  boundary (`subagentMessageId` change), flush the prior turn's
  accumulated content before creating the next in-thread assistant —
  covers text-only turns that never hit `persistToolBatch`.
- New `persistSubagentTextChunk` accumulates text/reasoning onto the
  run; `persistToolBatch` writes content alongside tools[] so DB
  sees both in one update (same pattern as main agent).
- New `finalizeSubagentRun` flushes pending content when the main-
  agent receives the spawn tool's `tool_result` — ensures the
  closing summary lands before `fetchAndReplaceMessages` refreshes
  from stale DB state.
- `onComplete` iterates `subagentRuns.keys()` and flushes any
  un-finalized runs, covering the CLI-crashed-mid-subagent edge case.

Tests:
- Adapter: replaced the "drops subagent text" test with two tests
  asserting text/reasoning ARE emitted with correct `subagent` peer
  context. New test covers the `Agent` spawn-tool variant.
- Executor: 4 new tests cover the Thread user message content
  population, subagent text accumulation into the in-thread assistant,
  non-leakage into main assistant content, and tool_result-triggered
  finalization. Total 29 executor tests pass.

E2E verified via Electron + CDP: fresh CC session → `Agent`-based
subagent → Thread created with `title="Run pwd command"`,
`metadata.subagentType="general-purpose"`, `role:'user'` seeded with
the Task prompt, Bash tool_use + result inside the thread.

Refs LOBE-7319, LOBE-7392

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

* 🐛 fix(heterogeneous-agents): refresh thread list when subagent Thread is lazy-created

Earlier Electron E2E repro: a subagent Thread born mid-stream landed
in DB correctly, but the topic sidebar only picked it up after the
user manually navigated topics / called `refreshThreads()` — the
SWR cache for the thread list (`SWR_USE_FETCH_THREADS`) wasn't
invalidated, so the new Thread stayed invisible until the next
cold fetch.

- `ensureSubagentRun` now accepts an optional `onThreadCreated`
  callback fired once per lazy Thread create. Kept as a callback
  (not a direct `store.refreshThreads` call) so the executor
  persistence logic stays decoupled from the Zustand store shape.
- `persistSubagentToolChunk` + `persistSubagentTextChunk` thread
  the callback through to `ensureSubagentRun`.
- Executor defines `onSubagentThreadCreated` once at run scope and
  passes it into all three subagent persist call sites. Calls
  `get().refreshThreads()` fire-and-forget — it's a no-op when the
  user has navigated away from the topic, so no need to block
  persist on cache refresh.

Two regression tests:
- Subagent-spawning run → `refreshThreads` called exactly once
- Non-subagent run (plain tool only) → `refreshThreads` NOT called

Refs LOBE-7319, LOBE-7392

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

*  feat(builtin-tool-claude-code): specialize Agent subagent Inspector + Render

CC's subagent-spawn tool arrives as `tool_use.name: 'Agent'`, not `Task` —
rename the apiName so the Inspector/Render registry actually matches the
stream. Inspector switches icon/label by `subagent_type` (Explore / Plan /
general-purpose / statusline-setup), with `description` surfaced in a chip;
new Render shows `prompt` and tool_result as labelled Markdown blocks that
can't fit in the folded header.

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

* 💄 style(workflow-collapse): unify expand toggle with ActionIcon

Replace the hand-rolled motion span + role="button" / keyboard-handler
expand toggle with a single @lobehub/ui ActionIcon — fewer a11y edge
cases to maintain and the icon/title/blockSize layout matches other
toolbar buttons in the group.

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

* 💄 style(builtin-tool-claude-code): inline-pad Edit diff container

Give the Edit render a small inline padding so the CodeDiff lines up
with the rest of the tool renders; zero-width flush-left was awkward
against the surrounding labelled blocks.

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

*  feat(heterogeneous-agents): interpolate agent name in running indicator

ContentLoading now renders "{name} is running" / "{name} 运行中" for
heterogeneous agent execution — previously it collapsed to the generic
"External agent running" so a user watching a long CC run couldn't tell
which external CLI was working (mattered once Codex landed as a sibling
adapter).

- Share `HETEROGENEOUS_TYPE_LABELS` (claude-code / codex) out of the
  heterogeneous-agents package so all consumers read one map; home
  Sidebar AgentItem switches to it and drops its inline copy.
- `conversationLifecycle.startOperation` passes
  `metadata.heterogeneousType` on the heterogeneous-exec operation so
  ContentLoading can resolve the label from the running op without
  re-deriving the adapter type from session state.
- New `operation.heterogeneousAgentFallback` key covers the (rare) case
  where the metadata is absent — keeps the dot loader labelled.

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

*  feat(claude-code): CC subagent Thread rendering pipeline

Closes the viewing loop for CC subagent runs: the main-topic Agent tool
row now links into the spawned Thread, the Thread's Portal view renders
with provenance + read-only affordances, and the sidebar surfaces which
entries are subagent-produced.

UX:
- Agent render gains a trailing "View / Collapse full subagent
  conversation" toggle. It looks up the Thread by
  `metadata.sourceToolCallId === toolCallId` and calls
  openThreadInPortal / closeThreadPortal — hidden until the executor
  lazy-creates the Thread on the first subagent event, so it never
  renders as a no-op.
- Portal Thread Header shows a `[icon] subagentType` Tag next to the
  title ("Explore" / "General purpose" / ...). Inspector's folded row
  already exposes the same detail, so the icon + label stays
  consistent across the two surfaces.
- Portal Thread Chat flips into read-only mode when
  `metadata.sourceToolCallId` is set: ChatInput is hidden (the
  external CLI owns the session — new turns have nowhere to go),
  `disableEditing` propagates to every message (no double-click to
  edit, no user action bar), and `useThreadActionsBarConfig` wipes
  `bar` + `menu` across assistant / assistantGroup / user roles.
- Sidebar ThreadItem on both /agent and /group routes renders a plain
  "Subagent" badge next to the title when
  `metadata.subagentType` is present. The type detail deliberately
  lives on the Thread Header, not here — sidebar space is tight.

Shared resolver:
- `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the
  Inspector into `packages/builtin-tool-claude-code/src/client/
  subagentTypes.ts` and re-export from the `/client` entry. Inspector
  + Portal Thread Header both consume it, so the icon/label stay in
  sync. Kept UI-level (LucideIcon | FC) rather than pushed into
  heterogeneous-agents, which is a pure-data package.
- Root package.json adds a direct dep on
  `@lobechat/builtin-tool-claude-code` so Portal Thread Header can
  import from `/client` (previously only transitive via builtin-tools).

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

*  test(workflow-collapse): mock @lobehub/ui ActionIcon + AccordionItem action slot

After the expand-toggle refactor to ActionIcon + the `action` prop on
AccordionItem, the test's module mocks were missing both: ActionIcon
wasn't exported from the @lobehub/ui mock, and AccordionItem dropped
`action` on the floor so the toggle never made it into the rendered
DOM. Restore both — ActionIcon renders as a real \`button\` with
aria-label so \`getByRole('button', { name })\` can still target it.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:48:16 +08:00
Arvin Xu c0db58e622 feat(topic): add completed status with dropdown action and filter (#14005)
*  feat(topic): add completed status with dropdown action and filter

- Surface ChatTopicStatus (active/completed/archived) on topic list items and pass to dropdown menu
- Add markTopicCompleted / unmarkTopicCompleted store actions wired into the topic item dropdown
- Show CheckCircle2 icon on completed topics in the sidebar list
- Add topicIncludeCompleted user preference (default false) and an "Include Completed" toggle in the topic filter menu (agent + group routes)
- Wire excludeStatuses and triggers filters through TopicModel, TRPC router, service, and store SWR keys so completed topics are excluded by default

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

* 🌐 i18n(topic): add zh-CN/en-US for completed status keys

Translate actions.markCompleted / actions.unmarkCompleted and filter.filter / filter.showCompleted for dev preview. CI's pnpm i18n will fill in remaining locales.

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

* ♻️ refactor(topic): scope completed exclusion to routes with the toggle

Move the topicIncludeCompleted preference read out of the chat-store useFetchTopics action and into the (main) agent/group sidebars where the "Include Completed" filter actually lives. Popup and mobile topic views call useFetchTopics without excludeStatuses, so completed topics remain reachable on surfaces that don't expose the toggle (e.g. the popup window for a deep-linked completed topic, the mobile TopicModal).

Also switch ChatTopicStatus imports in the topic item / dropdown files to @lobechat/types to match the rest of the topic-feature imports.

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

*  test(topic-model): cover excludeStatuses + triggers filters

Add cases to the TopicModel.query suite for the new params introduced alongside the topic.status column:
- triggers (positive trigger filter) on the container branch
- excludeStatuses on the container, agent, and groupId branches (verifies null status rows are still returned)
- status / completedAt are populated on returned items

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

* 💄 style(topic): move "Mark Completed" to top of agent topic dropdown

Promote the completed-status toggle to the first menu item, with a divider before favorite, so the most-used status action sits at the top of the dropdown.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:37:09 +08:00
YuTengjing 61224fe76c 🐛 fix(auth): return 401 for expired OIDC JWT instead of 500 (#14014) 2026-04-21 16:43:57 +08:00
Innei 8119789849 🐛 fix(model-bank): add repository metadata for provenance (#14018) 2026-04-21 15:59:55 +08:00
Innei 1ffd01a9eb 🐛 fix(model-bank): publish initial npm package publicly (#14017) 2026-04-21 15:50:28 +08:00
Innei 9d3696ceef 👷 build(model-bank): automate npm release (#14015) 2026-04-21 15:38:04 +08:00
LiJian 595193ce62 🐛 fix: clarify lobe-gtd and lobe-cron tool descriptions to prevent routing confusion (#14013)
When users say "daily task" or "routine", the model confused lobe-gtd (one-time todos) with lobe-cron (recurring automation), often falling back to user-memory or GTD instead of cron.

Fixes LOBE-7486

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 15:30:45 +08:00
LiJian 665b482390 🐛 fix: inject timezone and cron jobs list into cron tool system prompt (#14012)
* 🐛 fix: inject timezone and cron jobs list into cron tool system prompt

Add {{timezone}} to cron systemRole session_context so the model knows
the user's local timezone when creating scheduled tasks. Wire up the
{{CRON_JOBS_LIST}} placeholder that was already referenced in the
systemRole but never populated — now fetches the agent's existing cron
jobs via tRPC and injects them, following the same pattern as CREDS_LIST.

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

* 🐛 fix: limit cron jobs context to 4 items to save context window

Only inject a preview of up to 4 cron jobs into the system prompt.
When there are more, append a hint directing the model to call
listCronJobs API for the full list. This avoids bloating the context
window for agents with many scheduled tasks.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 15:25:55 +08:00
LiJian ca47d972a4 🐛 fix: fallback to skill activation when activateTools cannot find identifier (#14010)
* 🐛 fix: fallback to skill activation when activateTools cannot find identifier

When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails with "Not found" because skills and tools are separate
registries. Now activateTools falls back to activateSkill for identifiers
not found as tools, so skills can be activated regardless of which API the
LLM chooses to call.

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

* 🐛 fix: fallback to skill activation when activateTools cannot find identifier

When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails because skills and tools are separate registries.

Two changes:
1. ActivatorExecutionRuntime.activateTools() now falls back to activateSkill
   for identifiers not found as tools
2. selectActivatedSkillsFromMessages() now also extracts skills from
   activateTools messages (pluginState.activatedSkills[]), so downstream
   stepContext and execScript zip resolution work correctly

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:04:58 +08:00
YuTengjing c5db823a69 💄 style: add Kimi K2.6 to LobeHub-hosted card (#14006) 2026-04-21 11:40:15 +08:00
Arvin Xu 518358b95e 💄 style(todo-progress): vertically center collapsed header row (#13996)
Clear residual list-container margin/border when collapsed and slightly
increase bottom padding so the header sits on the bar's visual center.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:02:37 +08:00
sxjeru a15d962ae8 💄 style: add new Kimi K2.6 model (#14004)
*  feat(models): update AI models with new capabilities and pricing adjustments

*  feat(aiModels): add new AI models Kimi K2.6 and GLM-5.1 to ollamaCloud; enhance siliconCloud with Qwen3.6 35B A3B and update pricing and settings
2026-04-21 10:02:26 +08:00
Arvin Xu 569dcc8765 💄 style(thread): sync id allocation + ChatMiniMap polish (#14000)
*  feat(heterogeneous-agents): preserve CC subagent lineage in adapter

Claude Code tags subagent events (Agent / Task tool spawns) with
parent_tool_use_id pointing back at the outer tool_use. The adapter
used to flatten these, breaking the main-agent step tracker — each
subagent turn introduces a NEW message.id, which the adapter read as
"new main-agent step" and forced stream_end + stream_start(newStep),
producing orphan assistant bubbles and double-counted usage.

- ToolCallPayload.parentToolCallId carries the pointer to downstream
  consumers so they can group subagent inner tools under their parent.
- claudeCode.ts reads raw.parent_tool_use_id and:
  * skips main-agent step boundary on subagent message.id changes
  * skips model tracking for subagent events (the result event has
    the authoritative usage, would double-count otherwise)
  * drops subagent text / reasoning in this adapter pass — the
    subagent's final answer is delivered via the outer tool_result;
    verified against a real CC trace where 76 subagent assistant
    events carried only tool_use, zero text / thinking
  * stamps parentToolCallId onto subagent tool_use payloads
- 6 new unit tests cover lineage propagation, no newStep for subagent
  message.id changes, no turn_metadata emission, text/reasoning drop,
  main-agent resuming step boundary, and subagent tool_result
  passthrough.

Refs LOBE-7319, LOBE-7260

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

*  feat(types): foundation types for CC Task block (LOBE-7392)

Sets up the data shape for rendering CC subagent spawns as inline
`task` blocks inside the parent assistantGroup, replacing the
role:'task' message intermediary that was previously proposed in
PR #13928. Pure data layer — no DB schema migration, no new
columns.

- TaskBlock + AssistantContentBlock.tasks?: derived view that the
  MessageTransformer will populate by joining Threads onto the
  parent message's tool_use entries (follow-up commit). Carries
  threadId, subagentType, description, status — enough for the
  folded inline header without re-fetching the thread on every
  render pass.
- ThreadMetadata gains sourceToolCallId, subagentType, description.
  sourceToolCallId disambiguates parallel subagents that share a
  sourceMessageId (one assistant turn can spawn multiple Task
  tool_uses in one batch).
- CreateThreadParams.id + zod schema field + thread router
  passthrough lets clients allocate the threadId synchronously
  before the create mutation resolves. The CC adapter emits
  Task tool_use synchronously while the create call is async, so
  having the id up-front lets us persist subagent inner messages
  with the right threadId without a queue or blocking the stream.
- ClaudeCodeApiName.Task + TaskArgs match the CC tool_use shape
  (description, prompt, subagent_type) so executor / renderer can
  type the input safely.

Refs LOBE-7392

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

* ♻️ refactor: extract subagent assistant handler + drop ThreadMetadata.description

Two review-feedback cleanups on the LOBE-7392 foundation:

1. **Adapter — early-return + shared helper.** The main-agent path no
   longer carries `if (!isSubagentEvent)` guards; subagent events short-
   circuit into a dedicated `handleSubagentAssistant` that only extracts
   `tool_use` blocks, and both paths share a new `emitToolChunk` helper
   for the `tools_calling` + `tool_start` emission. Adding a new
   subagent suppress-rule (no model / no text / no step) now lives in
   one method instead of sprinkling guards across the main handler.

2. **ThreadMetadata — drop `description`, use `Thread.title`.** Thread
   already has a `title` column; storing the CC Task `description`
   input there is the canonical spot and removes the redundant metadata
   field. `TaskBlock.description` is collapsed into `TaskBlock.title`
   (single source), and the MessageTransformer will populate it from
   `thread.title` at read time. Also adds `status?: ThreadStatus` on
   `TaskBlock` so the renderer gets the processing / completed / failed
   state without a separate lookup.

Behavior unchanged — all 56 adapter tests still pass.

Refs LOBE-7392, LOBE-7319

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

* 🐛 fix(thread-router): translate id-collision into CONFLICT error

ThreadModel.create uses onConflictDoNothing() and returns undefined
when a caller-provided id collides with an existing row. With the
new client-side id passthrough (introduced in 16d73261f9 to let the
CC subagent executor allocate threadId synchronously), the original
router would silently insert a follow-up message with
threadId: undefined and return { threadId: undefined } — a data-
integrity regression flagged in PR review.

Translates the model's undefined return into TRPCError(CONFLICT) at
the router boundary so callers see an explicit error and can
regenerate their id and retry. The model layer is untouched —
onConflictDoNothing remains the right primitive for server-generated
ids where collisions are unreachable; the new validation only
applies when the router is the entry point.

- ensureThreadCreated helper extracted; both createThread and
  createThreadWithMessage routes funnel through it
- New thread model tests document the conflict behavior and
  caller-provided id passthrough that the router relies on (16/16
  pass)

Refs LOBE-7392

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

* 💄 feat(chat-minimap): user-message peek with in-place hover preview

- Filter ticks to user messages; fall back to last user when viewport is on assistant reply
- Replace per-tick popovers with one in-place panel that crossfades from rail center
- Drop arrow nav buttons (hover panel makes them redundant)
- Smooth sqrt width curve (5–16px) so short messages cluster naturally

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

* 💄 style(claude-code-todo): chip-style detail in inspector, plain header in render

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

*  revert(heterogeneous-agents): pull CC adapter subagent-lineage changes

The CC subagent-lineage adapter work (parent_tool_use_id routing,
parentToolCallId on ToolCallPayload, dedicated handleSubagentAssistant /
emitToolChunk helpers, 6 subagent tests) would ship before the thread
backend changes in this PR are deployed — online flows would see the new
payload field with no server to receive it.

Holding this PR to thread-router + foundation types only. The adapter
work is preserved on feat/lobe-7392-cc-adapter-followup and will ship
as a separate PR after this one is deployed.

Refs LOBE-7392, LOBE-7319

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:27:01 +08:00
Arvin Xu b4aa51baaa 🐛 fix: hetero-agent ToolSearch content + bot IM reply + titlebar polish (#13998)
* 💄 style(electron): use colorBgElevated for active title-bar tab

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

* 🔒 fix(bot): show operation id instead of raw error in IM failure reply

Replace the error message content in bot-facing failure replies with the
operation id so end users don't see raw runtime errors; errors are still
logged server-side for debugging and correlation via operation id.

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

* 🐛 fix(hetero-agent): extract tool_name from ToolSearch tool_reference blocks

CC CLI returns ToolSearch results as `tool_reference` content blocks with
only a `tool_name` field — no `text`/`content` — so the generic array
mapper collapsed every entry to '' and persisted empty content, keeping
the UI tool StatusIndicator stuck on the spinner (LOBE-7369).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:11:34 +08:00
Arvin Xu 16df8350fe 🐛 fix(user-panel): remove consecutive dividers in user panel menu (#13990)
When businessMenuItems (from cloud deployment) returns items that
include a trailing divider, and getDesktopApp prepends its own divider,
two dividers appear back-to-back between Credits and Get Desktop App.

Add a post-filter on mainItems that strips any consecutive divider,
regardless of which module injected them.
2026-04-20 22:29:24 +08:00
Innei a59a9c4943 feat(onboarding): structured hunk ops for updateDocument (#13989)
*  feat(onboarding): structured hunk ops for updateDocument

Extend `updateDocument` (and the underlying `@lobechat/markdown-patch`) with
explicit hunk modes so agents can unambiguously express deletes and inserts
instead of encoding them as clever search/replace pairs.

Modes: `replace` (default, backward-compatible), `delete`, `deleteLines`,
`insertAt`, `replaceLines`. Line-based modes use 1-based inclusive ranges
and are applied after content-based hunks, sorted by anchor line descending
so earlier lines stay stable. New error codes: `LINE_OUT_OF_RANGE`,
`INVALID_LINE_RANGE`, `LINE_OVERLAP`.

Onboarding document injection now prefixes each line with its 1-based number
(cat -n style) so the agent can cite line numbers when issuing line-based
hunks. Tool description, system role, and per-phase action hints updated to
teach the new shape.

* 🐛 fix(onboarding): align patchOnboardingDocument zod schema with structured hunks

The tRPC input schema still accepted only the legacy `{search, replace}` shape,
so agent calls using the new `insertAt`/`delete`/`deleteLines`/`replaceLines`
hunk modes were rejected before reaching `applyMarkdownPatch`. Switch to a
z.union matching MarkdownPatchHunk.

* 🐛 fix(markdown-patch): validate line ranges before overlap detection

Previously the overlap loop ran before per-hunk range validation, so an
invalid range (e.g. startLine=0 or endLine<startLine) combined with another
line hunk would be misreported as LINE_OVERLAP instead of the real
LINE_OUT_OF_RANGE / INVALID_LINE_RANGE. Validate each line hunk against the
baseline line count first, then run overlap detection on valid ranges only.
2026-04-20 21:17:28 +08:00
Innei a939962fa1 feat(env): add Kimi Coding Plan API environment variables (#13997)
*  feat(env): add Kimi Coding Plan API environment variables

Made-with: Cursor

* 📝 docs(env): document Kimi Coding Plan env vars in .env.example
2026-04-20 21:06:40 +08:00
Arvin Xu bb59b7391e 🚀 release: sync main branch to canary (#13995)
Automatic sync from main to canary. Merge conflicts detected.

**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260420-24659236264
git merge origin/main
# Resolve conflicts
git add -A && git commit
git push
```

> Do NOT merge canary into a main-based branch — always merge main INTO
the canary-based branch to keep a clean commit graph.
2026-04-20 20:03:28 +08:00
Arvin Xu 038070285a resolve merge conflicts 2026-04-20 17:41:43 +08:00
lobehubbot a0303b7c18 chore: merge main into canary (has conflicts to resolve) 2026-04-20 09:34:54 +00:00
Arvin Xu 3bcd581e7c 👷 build(database): add topic status and tasks automation mode (#13994) 2026-04-20 17:34:13 +08:00
Tsuki bacf422890 🐛 fix: remove desktop tracker legacy imports (#13993) 2026-04-20 15:39:12 +08:00
YuTengjing eb99190f9f feat(chat-input): gate prompt optimize by image output capability (#13992) 2026-04-20 15:04:12 +08:00
LiJian 18042b7d31 🐛 fix: remove systemRole truncation in getAgentDetail (#13988)
The 200-char truncation is no longer needed as the caller
already handles length limits.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:16 +08:00
Tsuki 5dd7cd7408 feat: add x ads tracking entry points (#13986)
*  feat: add x ads tracking entry points

* 🔨 chore: bump analytics to v1.6.2

* 🐛 fix: add auth analytics provider entry
2026-04-20 14:12:14 +08:00
Arvin Xu ed64e2b8af feat(electron): add Cmd+W/Cmd+T tab shortcuts with misc desktop polish (#13983)
* 💄 style(topic): darken project group folder label in sidebar

Previous `type='secondary'` on the group title was too faint against the
sidebar background; promote the text to default color for better
legibility and keep the folder icon at tertiary so it stays subtle.

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

* 💄 style(topic): use colorTextSecondary for project group title

Text's `type='secondary'` resolves to a lighter token than
`colorTextSecondary`; apply `colorTextSecondary` directly so the title
lands at the intended shade (darker than before, lighter than default).

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

*  feat(electron): show blue unread dot on tab when agent has unread badge

Mirror the sidebar agent unread badge on the corresponding browser-like tab as a subtle blue dot, so unread completions are visible even when the sidebar is out of view.

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

* 🐛 fix(electron): forward proxy env vars to spawned agent CLI

The main-process undici dispatcher set by ProxyDispatcherManager only
covers in-process requests — child processes like claude-code CLI never
saw the user's proxy config. Extract a shared `buildProxyEnv` so any CLI
spawn can merge HTTP(S)_PROXY / ALL_PROXY / NO_PROXY into its env.

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

*  feat(electron): close active tab on Cmd+W when multiple tabs are open

Cmd/Ctrl+W now closes the focused tab first and only closes the window when
a single tab (or none) remains.

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

*  feat(electron): add Cmd+T shortcut to open a new tab

Reuses the active tab's plugin context to create a same-type tab, mirroring
the TabBar + button behavior.

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

* 💄 style(electron): use container color for active tab background

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

*  test(electron): update Close menu item expectations for smart Cmd+W

Tests now assert the CmdOrCtrl+W accelerator and click handler instead of
the legacy role: 'close'.

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

* 🐛 fix(electron): drop const/store import from HeterogeneousAgentCtr

The controller previously pulled defaultProxySettings from @/const/store,
which chain-loads @/modules/updater/configs and electron-is — that breaks
any unit test that mocks `electron` without a full app shim. Make
buildProxyEnv accept undefined and read the store value directly.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:38:54 +08:00
Arvin Xu e7236c0169 🐛 fix(user): validate avatar URL and scope old-avatar deletion to owner (#13982)
Reject avatar values that aren't a base64 data URL, an absolute http(s) URL,
or an internal /webapi/user/avatar/<userId>/ path for the caller. Also
require the old avatar URL to live under the caller's own prefix (and
contain no '..') before removing it from S3.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:58:14 +08:00
YuTengjing fb471123fc feat: support model alias mapping for image and video runtimes (#13896) 2026-04-20 09:38:56 +08:00
Arvin Xu a0471d5906 feat(chat-input): branch ahead/behind badge + GitCtr refactor (#13980)
* 💄 style(todo-progress): replace green bar with inline progress ring

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

* 💄 style(chat-input): split branch and diff blocks, add changed-files popover

Branch now has its own hover tooltip for the full name; the diff stat is a
sibling block that opens a lazy-loaded popover listing changed files.

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

*  feat(chat-input): show ahead/behind commit count vs upstream

Adds a badge next to the branch chip showing commits pending push (↑, blue)
and pull (↓, red) against the branch's upstream tracking ref. Hidden when
no upstream is configured or both counts are zero. Refreshed on focus,
after checkout, and on manual refresh from the branch switcher.

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

* ♻️ refactor(desktop): extract git IPC methods into dedicated GitController

Moves detectRepoType, getGitBranch, getLinkedPullRequest, listGitBranches,
getGitWorkingTree{Status,Files}, getGitAheadBehind, and checkoutGitBranch out
of SystemCtr into a new GitCtr (groupName = 'git'). Shared helpers (resolveGitDir
/ resolveCommonGitDir / detectRepoType) become pure functions under utils/git.ts
so SystemCtr's selectFolder can still probe the picked folder without crossing
controller boundaries. Renderer side: new electronGitService wraps ipc.git.*,
and all six chat-input hooks plus BranchSwitcher are switched over.

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

* 💄 style(chat-input): inline ahead/behind arrows into branch chip

Moves the ↑/↓ counts out of a separate status block and inside the branch
trigger next to the label, so they sit with the branch they describe instead
of after the file-change badge. Tooltip folds into the branch tooltip (full
name · N to push · M to pull) so a single hover covers both pieces of info.

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

* 🐛 fix(desktop): parse git status with -z to avoid filename misparse

The previous getGitWorkingTreeFiles split every line on ' -> ' to detect
renames, but only R/C status codes emit that delimiter. Legitimate filenames
containing ' -> ' (or spaces, or embedded newlines) were misparsed — the
popover would report a truncated path or lose the entry entirely.

Switch both getGitWorkingTreeStatus and getGitWorkingTreeFiles to
`git status --porcelain -z`: NUL-terminated records, no C-style quoting,
no \n splitting hazards. Rename/copy entries emit two NUL-separated tokens
(DEST\0SRC) which we consume as a pair so counts and paths stay correct.

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

* 🐛 fix(todo-progress): hide stale todos when a new user turn starts

Add `selectCurrentTurnTodosFromMessages` that scopes the todos lookup
to messages after the last user message. The inline TodoProgress
component now uses it, so a completed 8/8 progress bar from a previous
operation no longer lingers across the next user turn.

The original `selectTodosFromMessages` is unchanged because the agent
runtime step context still needs cross-turn visibility of the plan.

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

* 🔒 fix(desktop): tighten GitHub remote detection to host position

Replace substring check `config.includes('github.com')` with a regex
anchored to URL host position so look-alikes like `evilgithub.com` and
`github.com.attacker.com` no longer classify as GitHub. Closes CodeQL
"Incomplete URL substring sanitization" on PR #13980.

Not a real security issue (the config file is local and the
classification only drives a UI icon), but the tightened check is
strictly more correct and silences the scanner.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 02:11:43 +08:00
Innei 3bd7f1f146 🐛 fix(electron): align TabBar left padding with NavPanel width on initial load (#13981)
🐛 fix(electron): align TabBar left padding with NavPanel width on initial load

Defer DraggablePanel mount in NavPanelDraggable until `isStatusInit` flips true
so defaultSize captures the hydrated `leftPanelWidth` instead of the pre-hydration
default. Before hydration, render a placeholder div matching the store's current
width so NavigationBar's live-read width stays aligned with the DOM. Also adds
a small paddingRight to NavigationBar for visual balance.

Without this, the TabBar's left edge drifted away from the NavPanel's right edge
whenever the user's persisted panel width differed from the 320px default.
2026-04-20 01:46:05 +08:00
Innei 730169e6b6 feat(electron): add + button to TabBar for new topic in active context (#13972)
*  feat(electron): add + button to TabBar to open new topic in active context

Introduce a pluggable `createNewTabAction` extension on RecentlyViewed
plugins so each page type can decide whether (and how) to spawn a new
tab from the active tab. Implemented for agent / agent-topic /
group / group-topic — clicking `+` creates a fresh topic under the
current agent/group and opens it as a new tab; other page types hide
the button by default.

*  feat(electron): support new tab from page context

Page plugin now implements `createNewTabAction`, creating a fresh
untitled document via `usePageStore().createPage` and opening it as
a new `page` tab.

* 🐛 fix(electron): refresh page list after creating a new page via TabBar +

`createPage` only hits the service; without refreshing the documents
list, the sidebar / PageExplorer wouldn't show the freshly-created
page until the next full reload.

* 🐛 fix(electron): highlight new page in sidebar when opened via TabBar +

Switch to `createNewPage`, which runs the full optimistic flow —
dispatches the new document into the sidebar list and sets
`selectedPageId` — so the nav item active state stays in sync with
the freshly-opened page tab.

* 🐛 fix(electron): dispatch real page doc into sidebar list for TabBar +

The earlier `createNewPage` approach relied on an optimistic temp
document that SWR revalidation can clobber before the real doc
replaces it, leaving the new page absent from the sidebar. Create
the page via `createPage` first, then synthesize a `LobeDocument`
from the server response and dispatch it into the list alongside
setting `selectedPageId` — the nav item now appears and highlights
in sync with the new tab.
2026-04-20 01:04:51 +08:00
Innei 6b6915d147 feat(onboarding): add preset agent naming suggestions (#13931)
*  feat(onboarding): add preset agent naming suggestions

* 🐛 fix(test): align AgentDocumentsGroup test assertions with title-first rendering

#13940 changed DocumentItem to prefer title over filename, but the
AgentDocumentsGroup tests from #13924 were still asserting on filename
strings. Update all text matchers to use titles (Brief / Example).
2026-04-20 00:54:11 +08:00
Rdmclin2 0213656565 🐛 fix: message gateway (#13979)
* fix: local webhook typing

* feat: add dormant status

* feat: add bot status tag

* feat: add bot connection status and refresh status

* feat: support bot status list refresh

* fix: bot status

* chore: add test timeout
2026-04-20 00:17:57 +08:00
Arvin Xu 8240e8685d 🐛 fix(desktop): repo-type detection for submodule/worktree + chat & sidebar polish (#13978)
* 🐛 fix(desktop): detect repo type for submodule and worktree directories

Route detectRepoType through resolveGitDir so directories where `.git`
is a pointer file (submodules, worktrees) are correctly identified as
git/github repos instead of falling back to the plain folder icon.

Fixes LOBE-7373

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

* 🐛 fix(desktop): reprobe repo type for stale recent-dir entries

The recents picker rendered `entry.repoType` directly from localStorage,
so any submodule/worktree entry cached while `detectRepoType` still
returned `undefined` stayed stuck on the folder icon even after the
main-process fix. Wrap each row icon in a component that calls
`useRepoType`, which re-probes missing entries and backfills the cache.

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

* 🐛 fix(chat-input): clear autocomplete hint on IME start to prevent freeze

Dispatch KEY_ESCAPE_COMMAND on compositionstart so the autocomplete
plugin removes PlaceholderInline/PlaceholderBlock nodes before the IME
begins composing. Composing next to those placeholder nodes caused the
editor to freeze during pinyin input with a visible hint.

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

* ♻️ refactor(topic-sidebar): split project grouping into ByProjectMode

Extracts project-specific group rendering from ByTimeMode into its own ByProjectMode folder, with a shared GroupedAccordion container. Project groups get a folder-icon column aligned with the topic item layout and a "new topic in {directory}" action.

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

* 🐛 fix(desktop): read config via commondir for linked worktrees

`resolveGitDir` returns `.git/worktrees/<name>/` for linked worktrees —
that dir has its own `HEAD` but no `config`, so `detectRepoType` still
returned `undefined` and worktrees missed the repo icon. Resolve the
`commondir` pointer first so `config` is read from the shared gitdir.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:56:39 +08:00
Arvin Xu 46df77ac3f 💄 style(tab-bar): blend inactive tabs with titlebar, show close icon by default (#13973)
* 💄 style(tab-bar): blend inactive tabs with titlebar, show close icon by default

Inactive tabs now use a transparent background and gain a subtle hover fill,
matching Chrome's tab chrome so the titlebar feels visually unified. The close
icon is always visible instead of fading in on hover, so users don't have to
hunt for it on narrow tabs.

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

* 🐛 fix(desktop): CMD+N now actually clears active topic on agent page

Previously the File → 新建话题 (CMD+N) handler only `navigate()`d to the
agent base path. When the user was on `/agent/:aid?topic=xxx`, this stripped
the URL param but `ChatHydration`'s URL→store updater skips `undefined`
values, so `activeTopicId` in the chat store was never cleared and the
subscriber would push the stale topic right back into the URL.

Call `switchTopic(null)` on the store directly when an agent is active so
the change propagates store→URL via the existing subscriber.

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

* 🐛 fix(hetero-agent): don't surface self-cancelled exits as runtime errors

User-initiated cancel/stop and Electron before-quit kill the agent process
with SIGINT/SIGTERM, producing non-zero exit codes (130/143/137). Mark
these via session.cancelledByUs so the exit handler routes them through
the complete broadcast — otherwise a user cancel or app shutdown would
look like an agent failure (e.g. "Agent exited with code 143" leaking
into other live CC sessions' topics).

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

*  feat(tab-bar): show running indicator dot on tab when agent is generating

Adds a useTabRunning hook that reads agent runtime state from the chat
store for agent / agent-topic tabs, and renders a small gold dot over
the tab avatar/icon while the conversation is generating. Other tab
types stay unaffected.

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

* 💄 style(claude-code): render ToolSearch select: queries as inline tags

Parses select:A,B,C into individual tag chips (monospace, subtle pill
background) instead of a comma-joined string, so the names of tools
being loaded read more clearly. Keyword queries keep the existing
single-highlight rendering.

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

*  feat(git-status): show +N ±M -K diff badge next to branch name

Surface uncommitted-file count directly in the runtime-config status bar
so the dirty state is visible at a glance without opening the branch
dropdown. Each segment is color-coded (added / modified / deleted) and
hidden when zero; a tooltip shows the verbose breakdown.

Implementation:
- Backend buckets `git status --porcelain` lines into added / modified /
  deleted / total via X+Y status pair
- New always-on useWorkingTreeStatus SWR hook (focus revalidation, 5s
  throttle) shared by GitStatus and BranchSwitcher — single fetch path
- BranchSwitcher's "uncommitted changes: N files" now reads `total`

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

* 🐛 fix(assistant-group): show only delete button while tool call is in progress

When the last child of an assistantGroup is a running tool call, `contentId`
is undefined and the action bar fell through to a branch that dropped the
`menu` and `ReactionPicker`, leaving a single copy icon with no overflow.
Replace the legacy `continueGeneration / delAndRegenerate / del` bar with a
del-only bar in this state — delete is the only action that makes sense
before any text block is finalized.

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

* 🐛 fix(conversation-flow): aggregate per-step nested metadata.usage in assistantGroup

After hetero-agent moved to per-step usage writes (`metadata: { usage: {...} }`),
the assistantGroup virtual message stopped showing the cumulative token total
across steps and instead surfaced only the last step's numbers.

Root cause: splitMetadata only recognised the legacy flat shape
(`metadata.totalTokens`, etc.) and didn't read the new nested shape, so each
child block went into aggregateMetadata with `usage: undefined`. The sum was
empty, and the final group inherited a single child's metadata.usage purely
because Object.assign collapsed groupMetadata down to the last child.

- splitMetadata now reads both nested (`metadata.usage` / `metadata.performance`)
  and flat (legacy) shapes; nested takes priority
- Add `'usage'` / `'performance'` to the usage/performance field sets in parse
  and FlatListBuilder so the nested objects don't leak into "other metadata"
- Regression test: multi-step assistantGroup chain sums child usages

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

* 💄 style(hetero-agent): tone down full-access badge to match left bar items

The badge was shouting in colorWarning + 500 weight; reduce to
colorTextSecondary at normal weight so it sits at the same visual rank
as the working-dir / git buttons on the left. The CircleAlert icon
still carries the warning semantics. Also force cursor:default so the
non-interactive label doesn't pick up an I-beam over its text.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:53:22 +08:00
Arvin Xu 6ca5fc4bdc feat(hetero-agent): Claude Code runtime, cwd, and sidebar polish (#13970)
*  feat(hetero-agent): synthesize pluginState.todos from CC TodoWrite

Adapter now translates Claude Code's declarative TodoWrite tool_use input into the shared StepContextTodos shape and attaches it to tool_result. Selector drops the GTD identifier filter so any producer honoring pluginState.todos lights up the TodoProgress card.

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

* 🐛 fix(hetero-agent): skip TodoWrite pluginState synthesis on error results

A failed TodoWrite (is_error=true) means the snapshot was never applied on CC's side. Since selectTodosFromMessages now picks the latest pluginState.todos from any producer, leaking a failed-write snapshot could overwrite the live todo UI with changes that never actually happened. Drain the cache either way so a retry with a fresh tool_use id doesn't inherit stale args.

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

* 🐛 fix(hetero-agent): prefer topic-level cwd on send; route UI changes to active topic

Topic-level workingDirectory now takes priority over agent-level on the
send path, matching what the topic is actually pinned to. The UI picker
writes to the active topic's metadata (not the agent default), and warns
before switching when doing so would invalidate an existing CC session.

*  feat(tab): reset tab cache when page type changes to stop stale metadata bleed

Switching a tab from one page type to another (e.g. agent → home) kept
the previous page's cached title/avatar, so the new page rendered with
the wrong header. Reset the cache on type change; preserve the merge
only when the type stays the same.

* 🐛 fix(hetero-agent): kill CC process tree on cancel so tool children exit

SIGINT to just the claude binary was leaving bash/grep/etc. tool
subprocesses running, which kept the CLI hung waiting on them. Spawn
the child detached (Unix) so we can signal the whole group via
process.kill(-pid, sig); use taskkill /T /F on Windows. Escalate
SIGINT → SIGKILL after 2s for tool calls that swallow SIGINT, and do
the same tree kill on disposeSession's SIGTERM path.

*  feat(hetero-agent): show "Full access" badge in CC working-directory bar

Claude Code runs locally with full read/write on the working directory
and permission mode switching isn't wired up yet — the badge sets that
expectation up-front instead of leaving users guessing. Tooltip spells
out the constraint for anyone who wants detail.

* ♻️ refactor(agent-list): show runtime name (Claude Code/Codex) instead of generic "External" tag

The "External" tag on heterogeneous agents didn't tell users which
runtime backs the agent — multiple CLI runtimes (Claude Code, Codex, …)
looked identical in the sidebar. Map the heterogeneous type to its
display name so the tag identifies the actual runtime, with the raw
type as a fallback for any future provider we haven't mapped yet.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:33:11 +08:00
Arvin Xu 77fd0f13f0 🐛 fix(hetero-agent): persist streamed text alongside tool writes; collapse workflow summary (#13968)
* 🐛 fix(hetero-agent): persist accumulated text alongside tools[] writes

Carry the latest streamed content/reasoning into the same UPDATE that
writes tools[], so the DB row stays in sync with the in-memory stream.
Without this, gateway `tool_end → fetchAndReplaceMessages` reads a
tools-only row and clobbers the UI's streamed text.

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

*  feat(workflow-summary): collapse summary when many tool kinds

When a turn calls >4 distinct tool kinds, list only the top 3 by count
and append "+N more · X calls total[ · Y failed]". Keeps the inline
summary scannable on long tool-heavy turns instead of running off the
line. Short turns keep the existing full list.

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

* 💄 style(claude-code): use chip style for Skill inspector name

Replace the colon+highlight text with a pill-shaped chip containing the
SkillsIcon and skill name. Gives the Skill activation readout visual
parity with other tool chips and prevents long skill names from
overflowing the inspector line.

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

*  test(agent-documents): assert on rendered title, not filename

#13940 changed DocumentItem to prefer document.title over filename, but
the sidebar test still expected 'brief.md' / 'example.com'. Align the
assertions with the current behavior so the suite is green on canary.

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

* 💄 style(tab-bar): show agent avatar on agent/topic tabs

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:13:46 +08:00
Arvin Xu ccbb75da06 ♻️ refactor(hetero-agent): persist per-step usage to each step assistant message (#13964)
* ♻️ refactor(hetero-agent): persist per-step usage to each step assistant message

Previously, usage tokens from a multi-step Claude Code run were accumulated
across all turns and written only to the final assistant message, leaving
intermediate step messages with no usage metadata.

Each Claude Code `turn_metadata` event carries per-turn token usage
(deduped by adapter per message.id), so write it straight through to the
current step's assistant message via persistQueue (runs after any in-flight
stream_start(newStep) that swaps currentAssistantMessageId). The `result_usage`
grand-total event is intentionally dropped — applying it would overwrite the
last step with the sum of all prior steps.

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

* ♻️ refactor(hetero-agent): normalize usage inside CC adapter (UsageData)

Follows the same principle as LOBE-7363: provider-native shape knowledge
stays in the adapter, executor only sees normalized events. The previous
commit left Anthropic-shape fields (input_tokens, cache_creation_input_tokens,
cache_read_input_tokens) leaking into the executor via `buildUsageMetadata`.

Introduce `UsageData` in `@lobechat/heterogeneous-agents` types with LobeHub's
MessageMetadata.usage field names. The Claude Code adapter now normalizes
Anthropic usage into `UsageData` before emitting step_complete, for both
turn_metadata (per-turn) and result_usage (grand total). Executor drops
`buildUsageMetadata` and writes `{ metadata: { usage: event.data.usage } }`
directly. Future adapters (Codex, Kimi-CLI) normalize their native usage into
the same shape; executor stays provider-agnostic.

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

* ♻️ refactor(hetero-agent): persist per-step provider alongside model

CC / hetero-agent assistant messages were writing `model` per step but
leaving `message.provider` NULL, so pricing/usage lookups could not key on
the adapter (e.g. `claude-code`, billed via CLI subscription rather than
raw Anthropic API rates).

CC adapter now emits `provider: 'claude-code'` on every turn_metadata event
(same collection point as model + normalized usage). Executor tracks
`lastProvider` alongside `lastModel` and writes it into:

- the step-boundary update for the previous step
- `createMessage` for each new step's assistant
- the onComplete write for the final step

Provider choice is the CLI flavor (what the adapter knows), not the wrapped
model's native vendor — CC runs under its own subscription billing, so
downstream pricing must treat `claude-code` as its own provider rather than
conflating with `anthropic`.

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

* 🐛 fix(hetero-agent): read authoritative usage from message_delta, not assistant

Under `--include-partial-messages` (enabled by the CC adapter preset), Claude
Code echoes a STALE usage snapshot from `message_start` on every content-block
`assistant` event — e.g. `output_tokens: 8` or `1` — and never updates that
snapshot as more output tokens are generated. The authoritative per-turn
total arrives on a separate `stream_event: message_delta` with the final
`input_tokens` + cache counts + cumulative `output_tokens` (e.g. 265).

The adapter previously grabbed usage from the first `assistant` event per
message.id and deduped, so DB rows ended up with `totalOutputTokens: 1` on
every CC turn.

Move turn_metadata emission from `handleAssistant` to a new `message_delta`
case in `handleStreamEvent`. `handleAssistant` still tracks the latest model
so turn_metadata (emitted later on message_delta) carries the correct model
even if `message_start` doesn't.

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

* 💄 style(extras-usage): fall back to metadata.usage when top-level is absent

The assistant Extras bar passes `message.usage` to the Usage component,
which conditionally renders a token-count badge on `!!usage.totalTokens`.
Nothing in the read path aggregates `message.metadata.usage` up to
`message.usage`, so the top-level field is always undefined for DB-read
messages — the badge never shows for CC/hetero turns (and in practice also
skips the gateway path where usage only lands in `metadata.usage`).

Prefer `usage` when the top-level field is populated, fall back to
`metadata.usage` otherwise. Both fields are the same `ModelUsage` shape, so
the Usage/TokenDetail components don't need any other change.

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

* ♻️ refactor(extras-usage): promote metadata.usage inside conversation-flow parse

The previous fix spread a `usage ?? metadata?.usage` fallback across each
renderer site that passed usage to the Extras bar. Consolidate: `parse`
(src/store → packages/conversation-flow) is the single renderer-side
transform every consumer flows through, so promote `metadata.usage` onto the
top-level `usage` field there and revert the per-site fallbacks.

UIChatMessage exposes a canonical `usage` field, but no server-side or
client-side transform populated it — executors write to `metadata.usage`
(canonical storage, JSONB-friendly). Doing the promotion in parse keeps the
rule in one place, close to where display shapes are built, and covers both
desktop (local PGlite) and web (remote Postgres) without a backend deploy.

Top-level `usage` is preserved when already present (e.g. group-level
aggregates) — `metadata.usage` is strictly a fallback.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:19:18 +08:00
Innei 2711aa9191 feat(desktop): add dedicated topic popup window with cross-window sync (#13957)
*  feat(desktop): add dedicated topic popup window with cross-window sync

Introduce a standalone Vite entry for the desktop "open topic in new window"
action. The popup is a lightweight SPA (no sidebar, no portal) hosting only
the Conversation, and stays in sync with the main window through a
BroadcastChannel bus.

- Add popup.html + entry.popup.tsx + popupRouter.config.tsx
- Add /popup/agent/:aid/:tid and /popup/group/:gid/:tid routes
- Reuse main Conversation/ChatInput; wrap in MarketAuth + Hotkeys providers
- Pin-on-top button in the popup titlebar (new windows IPC: set/isAlwaysOnTop)
- Group topic "open in new window" now uses groupId (previously misused agentId)
- Cross-window sync: refreshMessages/refreshTopic emit via BroadcastChannel;
  subscriber revalidates local SWR caches with echo-loop suppression
- Hide WorkingPanel toggle inside /popup (no WorkingSidebar present)
- RendererUrlManager dispatches /popup/* to popup.html in prod; dev middleware
  rewrites SPA deep links while skipping asset/module requests

* 💄 style(desktop): restore loading splash in popup window

* ♻️ refactor(desktop): replace cross-window sync with popup-ownership guard

The BroadcastChannel-based bidirectional sync between the main SPA and the
topic popup window had edge cases during streaming. Drop it in favour of a
simpler ownership model: when a topic is already open in a popup, the main
window shows a "focus popup" redirect instead of rendering a second
conversation.

- Remove src/libs/crossWindowBus.ts and src/features/CrossWindowSync
- Remove postMessagesMutation/postTopicsMutation calls from refresh actions
- Add windows.listTopicPopups + windows.focusTopicPopup IPC
- Main process broadcasts topicPopupsChanged on popup open/close; parses
  (scope, id, topicId) from the popup window's /popup/... path
- Renderer useTopicPopupsRegistry subscribes to broadcasts and fetches the
  initial snapshot; useTopicInPopup selects by scope
- New TopicInPopupGuard component with "Focus popup window" button
- Desktop-only index.desktop.tsx variants for (main)/agent and (main)/group
  render the guard when the current topic is owned by a popup
- i18n: topic.inPopup.title / description / focus in default + en/zh

* 🐛 fix(desktop): re-evaluate popup guard when topic changes

Subscribe to the popups array and derive findPopup via useMemo so scope changes (e.g. switching topic in the sidebar while a popup is open) correctly re-compute the guard and let the main window render the newly active topic.

* 🐛 fix(desktop): focus detached topic popup from main window

*  feat(desktop): add open in popup window action to menu for active topic

Signed-off-by: Innei <tukon479@gmail.com>

* 🎨 style: sort imports to satisfy simple-import-sort rule

*  feat(error): add resetPath prop to ErrorCapture and ErrorBoundary for customizable navigation

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor: restore ChatHydration in ConversationArea for web/mobile routes

Reintroduce ChatHydration component to agent and group ConversationArea
so that URL query sync (topic/thread) works on web and mobile routes,
not only on desktop entry files.

*  feat(electron): enforce absolute base URL in renderer config to fix asset resolution in popup windows

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-19 02:15:29 +08:00
Innei c213483a7a feat(workflow): tri-state completion status icon for WorkflowCollapse (#13952)
*  feat: add full-expand toggle to WorkflowCollapse with three-level expansion

- Replace boolean expanded with expandLevel: 'collapsed' | 'semi' | 'full'
- Add cyclic toggle button in header (ChevronDown / Maximize2 / Minimize2)
- Keep max-height scroll constraint in semi mode, remove it in full mode
- Update tests for three-level states and toggle behavior

*  feat: enhance WorkflowCollapse with animated expand toggle and refined icon behavior

- Introduced animated transitions for the expand toggle button using `motion` from `framer-motion`.
- Updated expand toggle logic to improve user experience with clearer icon states.
- Removed unused `ChevronDown` icon and adjusted expand toggle label conditions.
- Added constants for toggle icon size and transition settings for better maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* test: fix WorkflowCollapse tests for animated toggle behavior

* feat(workflow): tri-state completion status icon for WorkflowCollapse

Replace binary errorPresent with getWorkflowCompletionStatus:
- success → green Check
- partial failure → yellow AlertTriangle
- all failed → red X

Adds unit tests for all three states.

* fix(workflow): address Codex review feedback

- Add workflow.collapse / workflow.expandFull locale keys
- Make expand toggle keyboard-accessible (tabIndex + Enter/Space)

* refactor(workflow): replace nested ternary with switch for statusIcon

* 🌐 fix(workflow): remove hardcoded defaultValue from i18n keys

Addresses Codex review: per AGENTS.md i18n rule, user-facing strings
should live in locale files, not as defaultValue fallbacks.

- Remove defaultValue from t('workflow.expandFull') and t('workflow.collapse')
- Update test mock to include the new keys so tests remain green

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-19 01:23:51 +08:00
Arvin Xu 4e5db98ffc ♻️ refactor(agent-documents): fix title/documentId flow + split Inspector per action (#13940)
- extract H1 from markdown content as document title (stripped from content)
- use title verbatim as filename (no extension); simplify dedup to `-2`, `-3`
- AgentDocumentModel.create accepts optional title; falls back to filename
- ExecutionRuntime createDocument returns documents.id (not agentDocuments.id)
  as state.documentId so the portal can resolve the row for openDocument
- sidebar DocumentItem prefers title over filename
- split AgentDocumentsInspector into 11 per-apiName components (Notebook pattern)
- tests: filename util (13), ExecutionRuntime wiring (5), updated model + service

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:06:06 +08:00
Arvin Xu b909e4ae20 💄 style(hetero-agent): add hetero-mode actions bar (#13963)
*  feat(hetero-agent): add hetero-mode actions bar with copy/delete only

Hide edit, regenerate, branching, translate, tts, share and delAndRegenerate
for heterogeneous-agent sessions where these actions don't apply. Introduce
`mode: 'hetero'` on MessageActionsConfig and dispatch to dedicated Hetero
action bars for user, assistant, and assistant-group messages.

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

* ♻️ refactor(conversation): replace per-role action hooks with declarative action registry

Replace the 4 duplicate per-role action hooks (useUserActions / useAssistantActions
/ useGroupActions / Task.useAssistantActions) and the 4 copies of
stripHandleClick / buildActionsMap / dispatch logic with a single registry +
universal MessageActionBar renderer.

Each action (copy / del / edit / regenerate / delAndRegenerate /
continueGeneration / translate / tts / share / collapse / branching) is now a
standalone module under components/MessageActionBar/actions/. Config is
declarative — string slot keys (e.g. ['copy', 'divider', 'del']) resolved
against the registry at render time.

Hetero-agent sessions drop the special mode flag; they just declare copy-only
slot lists via config. Dev-mode branching becomes a registry key instead of a
factory.

Deletes ErrorActionsBar (handled in-place via slot lists), the dead
Supervisor/Actions folder, and the HeteroActionsBar scaffold introduced in
the previous commit.

Net: -1900 lines, one place to add a new action.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:16:48 +08:00
Rdmclin2 7fe751eaec feat: billboard in sidebar (#13962)
* feat: support billboard

* feat: support BillBoard display

* fix: carousel dot style

* chore: adjust Anouncements copy

* feat: add annoucements animations

* feat: support  i18n and show less and more

* fix: notification copy

* chore: remove show less and show more

* feat:support Billboard title i18n

* fix: show billboard in time window

* feat: add  schema validation

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: test case

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-19 00:00:34 +08:00
Arvin Xu f38dcc4cfc 🐛 fix(cc): persist workingDirectory when CC topic is created (#13956)
Hetero-agent topic creation went through `aiChat.sendMessageInServer`'s
`newTopic` payload, which had no metadata field, so the topic row was
inserted with `metadata.workingDirectory = NULL`. Today the only writer
is the post-execution `updateTopicMetadata` in `heterogeneousAgentExecutor`
— that never lands when CC is cancelled or errors before completion, and
in the meantime the topic is missed by By-Project grouping and `--resume`
cwd verification has nothing to compare against.

Source the cwd at the start of the hetero branch and thread it through
`newTopic.metadata`, so the binding is set at insert time. The post-exec
update still runs to record `ccSessionId` (and is now a no-op for cwd).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:08:15 +08:00
Arvin Xu 30e93ada67 ♻️ refactor(hetero-agent): rename ccSessionId to heteroSessionId (#13961)
CC-specific naming leaked into a field/module that's meant to be shared
across heterogeneous agent adapters. Rename to a provider-neutral id so
new adapters can reuse the topic-level session binding without inheriting
CC terminology.

- ChatTopicMetadata.ccSessionId -> heteroSessionId
- resolveCcResume / CcResumeDecision -> resolveHeteroResume / HeteroResumeDecision
- ccResume.{ts,test.ts} -> heteroResume.{ts,test.ts}
- updateTopicMetadata zod schema + executor + conversationLifecycle callsites

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:52:08 +08:00
Arvin Xu bc9164ae4a 🐛 fix(cmdk): scope topic/message search to current agent (#13960)
Previously `agentId` was only used to boost relevance in SearchRepo,
so results from other agents still leaked into CMD+K when scoped to
an agent. Strictly filter topics/messages by `agentId` when provided,
and surface the active agent (avatar + title) as the scope chip so
users can see what the search is limited to.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:41:32 +08:00
Innei e990b08cc6 ♻️ refactor(types): break circular dep between types and const packages (#13948)
* ♻️ refactor(types): break circular dep between types and const packages

Types package should only carry types, not values. Moved hotkey type
definitions to be owned by @lobechat/types and removed the @lobechat/const
runtime dependency from @lobechat/types. @lobechat/const now imports its
hotkey types from @lobechat/types via import type and uses satisfies to
keep enum values aligned.

*  feat(types): add desktop hotkey types and configuration

Introduced new types for desktop hotkeys, including `DesktopHotkeyId`, `DesktopHotkeyItem`, and `DesktopHotkeyConfig`. These types facilitate the management of hotkeys in the desktop application, ensuring better type safety and clarity in the codebase. Updated documentation to reflect the relationship with `@lobechat/const` entrypoints.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-18 22:36:13 +08:00
Innei 5c82da7515 feat(onboarding): persist topic onboarding analytics snapshot (#13930)
*  feat(onboarding): persist topic onboarding analytics snapshot

* fix(onboarding): allow null in syncTopicOnboardingSession metadata option

Resolves TS2322 where topic?.metadata (ChatTopicMetadata | null | undefined)
was not assignable to metadata?: ChatTopicMetadata (undefined only).
The function already safely handles null via the ?? fallback, so widening
the parameter type is the minimal correct fix.

* fix(test): add ShikiLobeTheme to @lobehub/ui mock in WorkflowCollapse test

Resolves vitest error where @lobehub/editor tries to load
ShikiLobeTheme from the mocked module.
2026-04-18 22:08:56 +08:00
Arvin Xu 9218fbfcf3 💄 style(shared-tool-ui): wrap Bash inspector in a rounded chip (#13959)
💄 style(shared-tool-ui): wrap RunCommand inspector in a rounded chip

Put the terminal-prompt icon and the mono command text inside a single
pill-shaped chip (colorFillTertiary background) so the command reads as
one unit instead of two loose elements next to the "Bash:" label. Row
goes back to center-aligned since the chip has its own vertical padding.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:01:23 +08:00
Arvin Xu d581937196 feat(cc): account card, topic filter, and CC integration polish (#13955)
* 💄 style(error): refine error page layout and stack panel

Replace Collapse with Accordion for a clickable full-row header, move
stack below action buttons as a secondary branch, and wrap in a Block
that softens to filled when collapsed and outlined when expanded.

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

* 💄 style(cc): boost topic loading ring contrast in light mode

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

* 💄 style(error): reload page on retry instead of no-op navigate

The retry button called navigate(resetPath) which often landed on the
same path and re-triggered the same error, feeling broken. Switch to
window.location.reload() so the error page actually recovers, and drop
the now-unused resetPath prop across route configs.

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

* 🐛 fix(cc-agent): send prompt via stdin stream-json to avoid CLI arg parsing

Previously the Claude Code prompt was appended as a positional CLI arg,
so any prompt starting with `-` / `--` (dashes, 破折号) got
misinterpreted as a flag by the CC CLI's argparser.

Switch the claude-code preset to `--input-format stream-json` and write
the prompt as a newline-delimited JSON user message on stdin for all
messages (not just image-attached ones). Unifies the image and text
paths and paves the way for LOBE-7346 Phase 2 (persistent process +
native queue/interrupt).

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

* ♻️ refactor(cc): extract per-tool inspectors into Inspector/ folder

Mirrors the Inspector/<Tool>/index.tsx convention used by builtin-tool-skills,
builtin-tool-skill-store, and builtin-tool-activator.

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

* ♻️ refactor(cc): flatten Inspector/ to per-tool tsx files

Drop the per-tool subfolder wrapper (Inspector/Edit/index.tsx → Inspector/Edit.tsx)
since each tool is a single file — no co-located assets to justify the folder.

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

*  feat(topic): add filter with By project grouping and sort-by option

Split the legacy topicDisplayMode enum into independent topicGroupMode
(byTime / byProject / flat) and topicSortBy (createdAt / updatedAt), and
surface them from a new sidebar Filter dropdown. Adds groupTopicsByProject
so topics can be grouped by their workingDirectory, with favorites pinned
and the "no project" bucket placed last.

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

*  feat(cc): show Claude Code account and subscription on profile

Add a getClaudeAuthStatus IPC that shells out to claude auth status --json,
and render the returned email + subscription tag on the CC Status Card.
The auth fetch runs independently of tool detection so a failure can't
flip the CLI card to unavailable.

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

* 💄 style(home): show running spinner badge on agent/inbox avatars

Replace NavItem's generic loading state with a bottom-right spinner badge
on the avatar, so a running agent stays clearly labelled without hiding
the avatar. Inbox entries switch to per-agent isAgentRunning so only the
actively running inbox shows the badge.

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

* 💄 style(cc): default-expand Edit and Write tool renderers

Add ClaudeCodeApiName.Edit and Write to ClaudeCodeRenderDisplayControls
so their inspectors render expanded by default, matching TodoWrite.

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

* 🔧 chore(cc): drop default system prompt when creating Claude Code agent

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

* Update avatar URL for Claude Code

*  test(workflow-collapse): stub ShikiLobeTheme on @lobehub/ui mock

@lobehub/editor's init code reads ShikiLobeTheme from @lobehub/ui, which
some transitive import pulls in during the test. Add the stub to match
the pattern used in WorkingSidebar/index.test.tsx.

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

* 🐛 fix(cc): fall back to Desktop path instead of `/` when no cwd is set

- Selector prefers desktopPath over homePath before it resolves nothing,
  so the renderer always forwards a sensible cwd.
- Main-process spawn mirrors the same fallback with app.getPath('desktop'),
  covering cases where Electron is launched from Finder (parent cwd is `/`).

Fixes LOBE-7354

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

* 🐛 fix(topic): use remote app origin for topic copy link

Desktop 下 window.location.origin 是 app://renderer,复制出来的链接无法分享。
改用 useAppOrigin(),与分享链接保持一致(web 用 window.location.origin,
desktop 用 electron store 的 remoteServerUrl)。

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:58:50 +08:00
Innei 568389d43f ♻️ refactor(web-onboarding): rename doc tools and drive incremental persona writes (#13933)
* ♻️ refactor(web-onboarding): rename doc tools and drive incremental persona writes

- Rename writeDocument (full rewrite) and updateDocument (SEARCH/REPLACE patch) so tool
  names match model intuition; the old updateDocument (full) is now writeDocument and the
  old patchDocument (patch) is now updateDocument.
- Rework systemRole, toolSystemRole, and OnboardingActionHintInjector to require per-turn
  persistence: seed persona on user_identity, patch on every discovery turn where a new
  fact is learned, and stop the one-shot full-write pattern.
- Add a Pre-Finish Checklist so agents verify soul/persona reflect the session before
  calling finishOnboarding.

Eval (deepseek-chat, web-onboarding-v3):
- fe-intj-crud-v1: write=2, updateDocument=6/6 success
- extreme-minimal-response-v1: write=2, updateDocument=4/4 success
- Previously 0 patch usage; now patch dominates incremental edits.

* 🐛 fix(web-onboarding): decouple fullName persistence from role discovery

Persona seeding and saveUserQuestion(fullName) were gated on learning both
name AND role in the same turn, which regressed the prior behavior of saving
the name the moment it was provided. If the user shared only a name (or left
early before role was clarified), the agent could skip the save and end
onboarding with missing identity data.

Split the hint:
1. saveUserQuestion(fullName) fires as soon as the name is known, regardless
   of role.
2. Persona seeding fires on ANY useful fact (name alone, role alone, or both).

Thanks to codex review for catching this.
2026-04-18 20:02:39 +08:00
Arvin Xu 7d5889a7ed feat(heterogeneous-agent): git-aware runtime config + topic rename modal + inspectors (#13951)
*  feat(cc-desktop): git-aware runtime config + topic rename modal + inspectors

Cluster of desktop UX improvements around the Claude Code integration:

- CC chat input runtime bar: branch switcher, git status, and a richer
  working-directory bar powered by a new SystemCtr git API
  (branch list / current status) and `useGitInfo` hook.
- Topic rename: switch to a dedicated RenameModal component; add an
  auto-rename action in the conversation header menu.
- ToolSearch inspector for the CC tool client.
- Shared DotsLoading indicator.
- Operation slice tidy-ups for CC flows.

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

* ♻️ refactor(types): rename heterogeneous provider type `claudecode` → `claude-code`

Align the type literal with the npm/CLI naming convention used elsewhere
(@lobechat/builtin-tool-claude-code, claude-code provider id) so the union
matches the rest of the codebase.

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

* 💄 style(cc-desktop): polish TodoWrite labels, branch switcher refresh, and chat input affordances

- TodoWrite render + inspector: i18n the header label (Todos / Current step
  / All tasks completed), surface the active step inline as highlighted text,
  and switch the in-progress accent from primary to info for better contrast.
- BranchSwitcher: move the refresh button into the dropdown's section header,
  switch the search and create-branch inputs to the filled variant, and
  reuse DropdownMenuItem for the create-branch entry instead of a custom
  footer chip.
- GitStatus: drop the inline refresh affordance (now lives in the switcher),
  collapse trigger styles, and split the PR badge with its own separator.
- WorkingDirectory / WorkingDirectoryBar: tighten paddings and gaps so the
  runtime config row reads at a consistent height.
- InputEditor: skip inline placeholder completion when the cursor is not at
  end of paragraph — inserting a placeholder mid-text triggered nested
  editor updates that froze the input.

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

* 🐛 fix(cc-desktop): probe repoType for working dirs not cached in recents

GitStatus was gated on the `repoType` stored in `recentDirs`, but legacy
string entries and agent-config-driven paths that never went through the
folder picker have no cached `repoType`. As a result, branch / PR status
silently disappeared for valid git repos until users re-selected the
folder.

Promote `detectRepoType` to a public IPC method and add a `useRepoType`
hook that uses the cached value as a fast path, otherwise probes the
filesystem via SWR and backfills the recents entry so subsequent reads
hit cache. Both runtime config bars (CC mode + heterogeneous chat input)
now resolve `repoType` through the hook.

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

* 💄 style(shared-tool-ui): rework Bash/Grep/Glob inspector rows

- RunCommand: terminal-prompt icon + mono command text instead of underline highlight
- Grep: split pattern by `|` into mono tag chips
- Glob: single mono tag chip matching Grep
- Switch rows to baseline alignment so the smaller mono text lines up with the label

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

* 🐛 fix(DotsLoading): allow optional color in styles params

The Required<StyleArgs> generic forced color to string, but it's only
defaulted at the CSS level via fallback to token.colorTextSecondary.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:40:39 +08:00
Arvin Xu 5dc94cbc45 feat(cc-agent): improve for CC integration mode (#13950)
*  feat(cc-agent-profile): swap model/skills pickers for CC CLI status in CC mode

When an agent runs under the Claude Code heterogeneous runtime, its model and tools are
owned by the external CLI, so the profile page's model selector and integration-skills
block are misleading. Replace them with a card that re-detects `claude --version` on
mount and shows the resolved binary path — useful when CLAUDE_CODE_BIN or similar
points at a non-default CLI.

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

* 💄 style(cc-agent-profile): hide cron for CC agent and polish render previews

- Hide cron sidebar entry when current agent is heterogeneous (CC)
- Allow model avatar in agent header emoji picker
- Add padding to Glob/Grep/Read/Write preview boxes for consistent spacing
- Simplify NavPanelDraggable by removing slide animation layer

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

* ♻️ refactor(shared-tool-ui): extract ToolResultCard for Read/Write/Glob/Grep renders

Hoist the shared card shell (icon + header + preview box) into
@lobechat/shared-tool-ui/components so the four Claude Code Render
files no longer duplicate container/header/previewBox styles.

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

* 💄 style(agent-header): restyle title and expand actions menu

Bold the topic title, render the working directory as plain text (no chip/icon), move the "..." menu to the left, and expand it with pin/rename/copy working directory/copy session ID/delete. Fall back to "New Topic" when no topic is active.

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

* 💄 style(topic-list): replace spinning loader with ring-and-arc loading icon

Adds a reusable RingLoadingIcon (static track + rotating arc, mirroring the send-button style) and swaps the topic-item loader over to it so the loading state reads as a polished ring rather than a thin spinning dash.

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

* 💄 style(topic-list): switch unread indicator to a radar ping effect

Replaces the glowing neon-dot pulse with a smaller 6px core dot plus a CSS-keyframe ripple ring that scales out and fades, giving the unread marker a subtler, more refined cadence.

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

* 💄 style(cc-chat-input): drop file upload in CC mode, surface typo toggle

Claude Code brings its own file handling and knowledge context, so the
paperclip dropdown only showed "Upload Image" + a useless "View More"
link — confusing and not clean. Replace fileUpload with typo in the
heterogeneous chat input, and fold ServerMode back into a single
Upload/index.tsx now that the ClientMode/ServerMode split is gone.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:53:58 +08:00
Arvin Xu 13fe968480 feat: claude code intergration polish (#13942)
* 🐛 fix(cc-resume): guard resume against cwd mismatch (LOBE-7336)

Claude Code CLI stores sessions per-cwd under `~/.claude/projects/<encoded-cwd>/`,
so resuming a session from a different working directory fails with
"No conversation found with session ID". Persist the cwd alongside the session
id on each turn and skip `--resume` when the current cwd can't be verified
against the stored one, falling back to a fresh session plus a toast explaining
the reset.

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

*  feat(cc-desktop): Claude Code desktop polish + completion notifications

Bundles the follow-on UX improvements for Claude Code on desktop:

- Completion notifications: CC / Codex / ACP runs now fire a desktop
  notification (when the window is hidden) plus dock badge when the turn
  finishes, matching the Gateway client-mode behavior.
- Inspector + renders: add Skill and TodoWrite inspectors, wire them
  through Render/index + renders registry, expose shared displayControls.
- Adapter: extend claude-code adapter with additional event coverage and
  regression tests.
- Sidebar / home menu: clean up Topic list item and dropdown menu, rename
  "Claude Code Agent" entry point to "Add Claude Code" across EN/ZH.
- Assorted: NotificationCtr, Browser, WorkflowCollapse, ServerMode upload,
  agent/tool selectors — small follow-ups surfaced while building the
  above.

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

*  test(browser): mock electron.app for badge-clear on focus

Browser.focus handler now calls app.setBadgeCount / app.dock.setBadge to
clear the completion badge when the user returns. Tests imported the
Browser module without exposing app on the electron mock, causing a
module-load failure.

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

*  feat(cc-topic): folder chip + unify cwd into workingDirectory (#13949)

 feat(cc-topic): show bound folder chip and unify cwd into workingDirectory

Replace the separate `ccSessionCwd` metadata field with the existing
`workingDirectory` so a CC topic's bound cwd has one source of truth:
persisted on first CC execution, read back by resume validation, and
surfaced in a clickable folder chip next to the topic title on desktop.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:42:00 +08:00
Innei a98d113a80 feat: add full-expand toggle to WorkflowCollapse with three-level expansion (#13906)
*  feat: add full-expand toggle to WorkflowCollapse with three-level expansion

- Replace boolean expanded with expandLevel: 'collapsed' | 'semi' | 'full'
- Add cyclic toggle button in header (ChevronDown / Maximize2 / Minimize2)
- Keep max-height scroll constraint in semi mode, remove it in full mode
- Update tests for three-level states and toggle behavior

*  feat: enhance WorkflowCollapse with animated expand toggle and refined icon behavior

- Introduced animated transitions for the expand toggle button using `motion` from `framer-motion`.
- Updated expand toggle logic to improve user experience with clearer icon states.
- Removed unused `ChevronDown` icon and adjusted expand toggle label conditions.
- Added constants for toggle icon size and transition settings for better maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* test: fix WorkflowCollapse tests for animated toggle behavior

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-18 12:25:00 +08:00
Innei 9a2ee8a58f feat(onboarding): add wrap-up button for agent onboarding (#13934)
Let users finish agent onboarding explicitly once they've engaged
enough, instead of waiting for the agent to trigger finishOnboarding.

- New WrapUpHint component above ChatInput; shows in summary phase or
  discovery phase after ≥3 user messages
- Confirm modal before finish; reuses existing finishOnboarding service
- Tightened Phase 2 (user_identity) system prompt: MUST save fullName
  before leaving phase, handle ambiguous name responses explicitly
2026-04-18 11:58:49 +08:00
LobeHub Bot 326ca352b1 🌐 chore: translate non-English comments to English in oidc-provider (#13945)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 11:41:26 +08:00
Junghwan 2c43f409d9 🐛 fix(desktop): sanitize heterogeneous-agent attachment cache filenames (#13937)
* Keep heterogeneous-agent attachment cache writes inside the cache root

The desktop heterogeneous-agent controller used raw image ids as path
segments for cache payload and metadata files. Path-like ids could
escape the intended cache directory, and pre-seeded traversal targets
could be treated as cache hits. Hashing the cache key removes any path
semantics from user-controlled ids while preserving stable cache reuse.
A regression test covers both out-of-root write prevention and ignoring
pre-seeded traversal cache files.

Constraint: The fix must preserve deterministic cache hits without trusting user-controlled path segments
Rejected: path.basename(image.id) | collapses distinct ids onto the same filename and leaves edge-case normalization concerns
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any future cache layout change must keep user-controlled identifiers out of direct filesystem path composition
Tested: Custom local reproduction against current controller source; custom local validation against patched source; regression test added for desktop controller path handling
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

* Keep heterogeneous-agent cache regression aligned with runtime MIME behavior

The traversal regression test uses a data:text/plain URL under the desktop
node test environment, so the controller returns text/plain from the fetch
response headers. The expectation now matches the actual runtime behavior
instead of assuming the image/png fallback path.

Constraint: The regression should validate cache isolation rather than rely on an incorrect MIME fallback assumption
Rejected: Mock fetch in the regression test | adds extra indirection without improving the path traversal coverage
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep this test focused on path safety and cache-hit behavior; avoid coupling it to unrelated transport mocks unless the controller logic changes
Tested: Local patched-controller validation harness; static review against desktop vitest node environment behavior
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

* Keep heterogeneous-agent cache regression isolated to the temp test namespace

The first regression test used a fixed traversal target name under the shared
system temp directory. Switching that escape target to a unique name derived
from the test's temporary appStoragePath preserves the same out-of-root check
while avoiding accidental interaction with unrelated files under /tmp.

Constraint: The regression must still verify escape prevention beyond appStoragePath without touching shared fixed temp paths
Rejected: Remove the out-of-root assertion entirely | weakens coverage for the exact traversal behavior this PR is meant to guard
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep filesystem regressions hermetic; if a test needs to reason about escaped paths, derive them from per-test temp namespaces whenever possible
Tested: Static review of resolved path behavior before/after the change
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

---------

Co-authored-by: OpenAI Codex <codex@example.com>
2026-04-18 00:54:32 +08:00
YuTengjing 4d7ca56c21 🔨 chore: split test-app shards and deprecate isOnboarded (#13938) 2026-04-18 00:23:01 +08:00
Arvin Xu 80ae553f0f 🔨 chore: stream token-level deltas via --include-partial-messages (#13929)
 feat(cc-partial-messages): stream token-level deltas via --include-partial-messages

Enables Claude Code's --include-partial-messages flag so the CLI emits
token-level deltas wrapped in stream_event events. The adapter surfaces
these deltas as incremental stream_chunk events and suppresses the
trailing full-block emission from handleAssistant for any message.id
whose text/thinking has already been streamed.

Message-boundary handling is refactored into an idempotent
openMainMessage() helper so stepIndex advances on the first signal of a
new turn (delta or assistant), keeping deltas attached to the correct
step.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:12:47 +08:00
Arvin Xu 75b55edca1 feat: promote agent documents as primary workspace panel (#13924)
* ♻️ refactor: adopt Notebook list + EditorCanvas for agent documents

The agent working sidebar previously used a FileTree directory view and
a hand-rolled Markdown+TextArea editor with manual save. Agent documents
already back onto the canonical `documents` table via an FK, so they can
reuse the exact same rendering surface as Notebook.

- AgentDocumentsGroup: replace FileTree with a flat card list styled
  after Portal/Notebook/DocumentItem (icon + title + description + delete).
- AgentDocumentEditorPanel: drop the bespoke draft/save/segmented view
  logic; mount the shared <EditorCanvas documentId={doc.documentId}
  sourceType="notebook" /> inside an EditorProvider so auto-save and
  rich editing are handled by useDocumentStore.

*  feat: promote agent documents as the primary workspace panel

- Replace the agent-document sidebar with a Notebook-style list: pill
  filter (All/Docs/Web), per-item createdAt, globe icon for sourceType=web.
- Add a stable panel header "Resources" with a close button (small size,
  consistent with other chat header actions); no border divider.
- Wire clicks to the shared Portal Document view via openDocument(),
  retiring the inline AgentDocumentEditorPanel.
- Portal/Document/Header now resolves title directly from documentId
  via documentService.getDocumentById + a skeleton loading state.
- Portal top-right close icon switched to `X`.
- Layout: move AgentWorkingSidebar to the rightmost position; auto-collapse
  the left navigation sidebar while Portal is open (PortalAutoCollapse).
- Header: remove dead NotebookButton, drop the Notebook menu item; add a
  WorkingPanelToggle visible only when the working panel is collapsed.
- ProgressSection hides itself when the topic has no GTD todos.
- Builtin tool list removes Notebook; migrate CreateDocument Render and
  Streaming renderers to builtin-tool-agent-documents (notebook package
  kept for legacy rendering of historical tool calls).
- agent_documents list UI now reads from a separate SWR key
  (documentsList) so the agent-store context mapping doesn't strip
  documentId/sourceType/createdAt from the UI payload.
- i18n: add workingPanel.resources.filter.{all,documents,web},
  viewMode.{list,tree}, and the expanded empty-state copy; zh-CN
  translations seeded for preview.
- New local-testing reference: agent-browser-login (inject better-auth
  cookie for authenticated agent-browser sessions).

* update

* 🐛 fix: satisfy tsc strict i18next keys, remove duplicate getDocumentById, coerce showLeftPanel

* ♻️ refactor: graduate agent working panel out of labs
2026-04-17 23:04:59 +08:00
Arvin Xu 7981bab5bd 🐛 fix(auth): clear OIDC sessions when user signs out via better-auth (#13916)
🐛 fix(auth): clear current-browser OIDC session on sign-out

When a user signs out and signs back in as a different account,
the oidc-provider session cookie (_session) still references the
old accountId. The next /authorize silently reuses it, issuing
tokens for the wrong user.

Fix: add a POST /oidc/clear-session endpoint that:
1. Reads the _session cookie from the current request
2. Deletes the matching row in oidc_sessions (by primary key)
3. Expires the _session cookies in the response

The frontend logout action calls this endpoint *before* signOut()
while the better-auth session is still valid.

Only the current browser's OIDC session is affected — other
devices (desktop, CLI, mobile) keep their sessions intact.
2026-04-17 22:32:29 +08:00
Innei 03d2068a5d feat(onboarding): add feature flags and footer promotion pipeline (#13853)
*  feat(onboarding): enhance agent onboarding experience and add feature flags

- Added new promotional messages for agent onboarding in both Chinese and default locales.
- Updated HighlightNotification component to support action handling and target attributes.
- Introduced feature flags for agent onboarding in the configuration schema and tests.
- Implemented logic to conditionally display onboarding options based on feature flags and user state.
- Added tests for the onboarding flow and promotional notifications in the footer.

This update aims to improve the user experience during the onboarding process and ensure proper feature management through flags.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(home): add footer promotion pipeline with feature-flag gating

Extract resolveFooterPromotionState for agent onboarding vs Product Hunt promos.
Normalize isMobile boolean, refine HighlightNotification CTA layout, extend tests.

Made-with: Cursor

*  feat(locales): add agent onboarding promotional messages in multiple languages

Added new promotional messages for agent onboarding across various locales, enhancing the user experience with localized action labels, descriptions, and titles. This update supports a more engaging onboarding process for users globally.

Signed-off-by: Innei <tukon479@gmail.com>

* 💄 chore: refresh quick wizard onboarding promo

* 🐛 fix(chat): keep long mixed assistant content outside workflow fold

*  feat(onboarding): add agent onboarding feedback panel and service

LOBE-7210

Made-with: Cursor

*  feat(markdown-patch): add shared markdown patch tool with SEARCH/REPLACE hunks

Introduce @lobechat/markdown-patch util and expose patchDocument API on the
web-onboarding and agent-documents builtin tools so agents can apply
byte-exact SEARCH/REPLACE hunks instead of resending full document content.

*  feat(onboarding): prefer patchDocument for non-empty documents

Teach the onboarding agent (systemRole) and context engine
(OnboardingActionHintInjector) to prefer patchDocument over updateDocument
when SOUL.md or User Persona already has content, keeping updateDocument
reserved for the initial seed write or full rewrites.

* 🐛 fix(conversation): add rightActions to ChatInput component

Updated the AgentOnboardingConversation component to include rightActions in the ChatInput, enhancing the functionality of the onboarding conversation interface.

Signed-off-by: Innei <tukon479@gmail.com>

* Add specialized onboarding approval UI

* 🐛 fix(serverConfig): handle fetch errors in server config actions

Updated the server configuration action to include error handling for fetch failures, ensuring that the server config is marked as initialized when an error occurs. Additionally, modified the SWR mock to simulate error scenarios in tests.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(tests): update Group component tests with new data-testid attributes

Added data-testid attributes for workflow and answer segments in the Group component tests to improve test targeting. Adjusted the isFirstBlock property for consistency and ensured the component renders correctly with the provided props.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 21:14:27 +08:00
Zhijie He d6a47531c6 💄 style: add qwen3.6-flash/plus & pixverse-c1 support (#13923)
style: add qwen3.6-flash/plus & pixverse-c1 support
2026-04-17 19:46:49 +08:00
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
Neko 3fb6b0d8e1 🐛 fix(app): right panel should use stableLayout, bump @lobehub/ui to 5.9.0 (#13920)
🐛 fix(app): right panel should use stableLayout, bump @lobehub/ui to 5.9.0
2026-04-17 19:11:45 +08:00
Arvin Xu 34b60e1842 🔨 chore: return full brief data in task activities (#13914)
*  feat: return full brief data in task activities (LOBE-7266)

The activity feed for tasks previously emitted a stripped `brief` row that
concatenated `resolvedAction` and `resolvedComment` and omitted everything
BriefCard needs (taskId, topicId, agentId, cronJobId, agents, actions,
artifacts, readAt, resolvedAt, etc.). Map the full `BriefItem` into each
activity row and reuse `BriefService.enrichBriefsWithAgents` to populate
the participant avatars. The CLI and prompt formatter now compose the
action + comment display string themselves.

* 🐛 fix: degrade gracefully when brief agent enrichment fails

getTaskDetail was calling BriefService.enrichBriefsWithAgents inside
Promise.all without a fallback, so a failure in the agent-tree lookup
would reject the whole request — a regression vs. the existing
.catch(() => []) pattern used by other activity reads in this method.
Fall back to agentless briefs on error so the task detail keeps
rendering.
2026-04-17 19:10:48 +08:00
LiJian 828175f8f0 🐛 fix: add the lost tools into manual agent runtime mode (#13918)
* fix: slove the manual mode cant use some builtin tools

* refactor: change the active skill tools from lobe-activtor to  lobe-skill tools

* fix: only inject the avaiable skill when use the auto mode

* fix: update the desktop tools skill

* fix: add the some test to ensure the builin tools will use in manual mode
2026-04-17 17:02:53 +08:00
Arvin Xu 316349ea06 💄 style: remove 'Management' from API Key tab title (#13919)
fix: remove 'Management' from API Key tab title
2026-04-17 16:30:35 +08:00
Innei 2f4fbd35d4 🐛 fix: show success status for tool calls with no return value (#13905)
* 🐛 fix: show success status for tool calls with no return value

When a tool call completes without returning content, the status indicator
was incorrectly showing a loading spinner instead of a success checkmark.
This fix passes the isToolCalling operation state to StatusIndicator to
correctly determine when a tool has finished executing.

https://claude.ai/code/session_01EBaKqzVTeEmrUXgFdNk7WH

* 🐛 fix(conversation): improve tool execution status handling

Updated the logic for determining tool execution states in both the Tool and Inspector components. The changes ensure that the status indicator accurately reflects when a tool is actively processing, even if no result is returned. This prevents misleading loading indicators and enhances user experience during tool interactions.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(DocumentHistoryDiff): correct JSX syntax for CircleLoading component

Removed unnecessary semicolon from CircleLoading component in DocumentHistoryDiff to ensure proper rendering. This minor fix enhances code clarity and maintains JSX standards.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(ModeSwitch.test): refactor tests to improve readability and performance

Updated the ModeSwitch test suite by removing unnecessary async/await patterns, simplifying the mock configuration, and ensuring consistent cleanup after each test. These changes enhance the clarity and efficiency of the test cases for the onboarding mode switch functionality.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 16:12:35 +08:00
Innei 669cb98c3d 🐛 fix(conversation): restore markdown animation for first assistant group block (#13904)
Made-with: Cursor
2026-04-17 14:46:58 +08:00
LiJian 2824c826bd 🐛 fix: should inject the user Locals Language into systemRole (#13911)
* fix: should inject the user Locals Language into systemRole

* fix: slove the ts

* fix: update the snapshot test

* fix: update the test.ts

* fix: test fixed
2026-04-17 14:12:37 +08:00
YuTengjing d658daa95d 🐛 fix: strip temperature/top_p for Claude Opus 4.7 (#13909) 2026-04-17 11:47:22 +08:00
YuTengjing d707f60365 feat: add Claude Opus 4.7 with xhigh effort tier (#13903) 2026-04-17 02:55:02 +08:00
Arvin Xu 91428ea0d2 🔨 chore: persist ccSessionId in topic metadata for CC multi-turn resume (#13902)
🐛 fix: persist ccSessionId in topic metadata for CC multi-turn resume

The renderer writes `ccSessionId` to topic metadata after each Claude Code
execution so the next turn can spawn `claude --resume <id>`, but the server
zod schema on `updateTopicMetadata` didn't list `ccSessionId`, so zod silently
stripped it — every turn started a fresh CC session and lost prior context.
2026-04-17 01:50:23 +08:00
LobeHub Bot 3471d2bf74 🚀 release: sync main branch to canary (#13900)
* 🔖 chore(release): release version v2.1.50 [skip ci]

* 📝 docs: Update changelog docs and release skills (#13897) 

* 🔨 chore: update .vscode/settings.json (#13894)

* 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)

Made-with: Cursor

* 📝 docs: Update changelog docs and release skills (#13897)

- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor

---------

Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>

* 🐛 fix: resolve merge conflicts in sync main to canary

Restore canary versions of skill docs that were overwritten during
main-to-canary sync, keeping #13899 improvements intact.

---------

Co-authored-by: CanisMinor <i@canisminor.cc>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
Co-authored-by: Innei <tukon479@gmail.com>
2026-04-17 00:35:29 +08:00
Innei d2197f4c30 ♻️ refactor(desktop): consolidate global shortcuts (LOBE-7181) (#13880)
* ♻️ refactor(desktop): consolidate global shortcuts and remove default showApp hotkey

- Add desktopGlobalShortcuts.ts as single source for Electron + renderer defaults
- Wire ShortcutManager and store to DEFAULT_ELECTRON_DESKTOP_SHORTCUTS
- Use DesktopHotkeyId for @shortcut; drop local shortcuts barrel
- Stop re-exporting DESKTOP_HOTKEYS_REGISTRATION from hotkeys

Fixes LOBE-7181

Made-with: Cursor

*  feat(desktop): introduce new stubs for business constants and types

- Added `@lobechat/business-const` and `@lobechat/types` packages to support workspace dependency resolution.
- Updated `package.json` and `pnpm-workspace.yaml` to include new stubs.
- Refactored imports in `index.ts` to utilize the new constants structure.
- Enhanced `desktopGlobalShortcuts.ts` with improved type definitions for hotkeys.

This change streamlines the management of constants and types across the desktop application.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor(hotkeys): consolidate desktop global shortcut definitions (LOBE-7181)

Made-with: Cursor

*  feat(session, user): replace direct type imports with constants

- Updated session.ts to use constants for session types instead of direct imports from @lobechat/types.
- Updated user.ts to use a constant for the default topic display mode, enhancing consistency and maintainability.

This change improves code clarity and reduces dependencies on external type definitions.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 00:32:05 +08:00
Innei 35558cbea1 🐛 fix(desktop): prevent invalid proxy toggle saves (#13850)
* 🐛 fix(desktop): prevent invalid proxy toggle saves

* 🩹 fix: close proxy form ci gaps

*  style: enhance SaveBar component with updated styles and improved color variables

Signed-off-by: Innei <tukon479@gmail.com>

* 🩹 fix(test): increase ProxyForm test timeout and add explicit delay: null

CI runs with coverage instrumentation cause these form-interaction
tests to take ~4–6s each, exceeding the default 5000ms timeout.
Increase describe timeout to 10000ms and add { delay: null } to
all user.type() calls to keep them stable under coverage.

* 🩹 fix(test): resolve ProxyForm test type errors with user-event v14

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 00:30:50 +08:00
Neko fef6ed122a 🐛 fix(app): collapse button of agent working panel should be clickable (#13884) 2026-04-17 00:29:22 +08:00
lobehubbot 93603ae83b 🔖 chore(release): release version v2.1.51 [skip ci] 2026-04-16 15:42:58 +00:00
CanisMinor d87094236a 🚀 release: 20260416 (#13895)
# 🚀 LobeHub v2.1.50 (20260416)

**Release Date:** April 16, 2026\
**Since v2.1.49:** 107 commits · 101 merged PRs · 13 contributors

> This weekly release focuses on improving runtime stability and gateway
execution consistency, while making Home/Recents workflows faster to
navigate and easier to manage in daily use.

---

##  Highlights

- **Server-side Human Approval Flow** — Agent runtime now supports more
reliable approve/reject/reject-continue handling in gateway mode,
reducing stalled execution paths in long-running tasks. (#13829, #13863,
#13873)

- **Message Gateway End-to-End Hardening** — Gateway message flow, queue
handling, tool callback routing, and stop interruption behavior were
strengthened for better execution continuity. (#13761, #13816, #13820,
#13815)

- **Client Tool Execution in Gateway Mode** — Client-executor tools now
run more predictably across gateway and desktop callers, with improved
executor dispatch behavior. (#13792, #13790)

- **Home / Recents / Sidebar Upgrade** — Sidebar layout, custom sort,
recents operations, and profile actions were improved to reduce
navigation friction in active sessions. (#13719, #13812, #13723, #13739,
#13878, #13734)

- **Agent Workspace and Documents Expansion** — Working panel and agent
document workflows were expanded and polished for better day-to-day
agent operations. (#13766, #13857)

- **Provider and Model Compatibility Improvements** — Added GLM-5.1
support and refined model/provider edge-case handling, including schema
and error-path fixes. (#13757, #13806, #13736, #13740)

---

## 🏗️ Core Agent & Architecture

### Agent runtime and intervention lifecycle

- Added server-side human approval and improved runtime coordination
across approve/reject decision paths. (#13829, #13863)
- Improved interrupted-task handling and operation lifecycle consistency
to reduce half-finished runtime states. (#13714)
- Refined error classification and payload propagation so downstream
surfaces receive clearer actionable errors. (#13736, #13740)

### Execution model and dispatch behavior

- Introduced executor-aware runtime behavior to better separate
client/server tool execution semantics. (#13758)
- Improved tool/plugin resolution and manifest handling to avoid runtime
failures on malformed inputs. (#13856, #13840, #13807)

---

## 📱 Gateway & Platform Integrations

- Added message gateway support and strengthened queue/error behavior
for more stable cross-channel execution. (#13761, #13816, #13820)
- Improved gateway callback pipeline with protocol and API additions for
`tool_execute` / `tool_result`. (#13762, #13764, #13765)
- Improved bot/channel reliability and DM/slash handling in
Discord-related paths. (#13805, #13724)

---

## 🖥️ CLI & User Experience

- Improved CLI reliability across message/topic operations and
build/minify-related paths. (#13731, #13888)
- Added image-to-video options and improved command behavior for
generation workflows. (#13788)
- Improved desktop runtime behavior for remote fetch and Linux
notification urgency handling. (#13789, #13782)

---

## 🔧 Tooling

- Extracted gateway stream client into `@lobechat/agent-gateway-client`
to centralize protocol usage and reduce duplication. (#13866)
- Improved built-in tool coverage and runtime support, including GTD
server runtime and missing lobe-kb tools. (#13854, #13876)
- Updated skill and frontmatter consistency in workflow tooling.
(#13730)

---

## 🔒 Security & Reliability

- **Security:** Strengthened API key WS auth behavior and safer
serverUrl forwarding in gateway-related auth paths. (#13824)
- **Reliability:** Reduced runtime stalls by improving gateway
stop/interrupt and approval-state routing behavior. (#13815, #13863,
#13873)
- **Reliability:** Added defensive guards for malformed tool manifests
and non-string content edge cases. (#13856, #13753)

---

## 👥 Contributors

**101 merged PRs** from **13 contributors** across **107 commits**.

### Community Contributors

- @arvinxx - Runtime, gateway, and execution reliability improvements
- @Innei - Navigation, workflow UX, and desktop/CLI refinements
- @rdmclin2 - Sidebar, recents, and channel behavior updates
- @ONLY-yours - Tooling/runtime fixes and model execution compatibility
- @tjx666 - Model support and release/tooling maintenance
- @nekomeowww - Memory and search-path stability fixes
- @cy948 - CLI indexing and command flow fixes
- @octo-patch - Local system runtime edge-case fixes
- @djthread - Desktop runtime request reliability improvements
- @rivertwilight - Documentation and changelog updates
- @sudongyuer - Subscription/mobile support improvements
- @Zhouguanyang - Provider/model configuration correctness fixes
- @lobehubbot - Translation and maintenance automation support

---

**Full Changelog**: v2.1.49...v2.1.50
2026-04-16 23:40:02 +08:00
Innei f1d615fa9f feat(document): add history management and compare workflow (#13725)
* Add document history versioning and TRPC APIs

* 🩹 Improve document history patching for rekeyed editor nodes

* Refine PageEditor history timeline UI

* Enhance modal API documentation and update modal implementation guidelines. Introduce new modal components and migration notes for transitioning from legacy `@lobehub/ui` to `@lobehub/ui/base-ui`. Update version history localization for improved clarity in UI. Add new CompareModal components for document history comparison.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔥 chore(docs): remove document history tech spec

Made-with: Cursor

* Enhance document history management by introducing a 30-day limit for history queries and updating related APIs. Refactor history service methods to support new options for filtering history based on the saved date. Improve UI elements in the PageEditor history timeline for better user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* Add document history management features and improve API integration

- Introduced constants for document history retention and limits.
- Updated document history service to compact history based on new retention limits.
- Refactored PageEditor to utilize constants for document history limits.
- Added new TRPC router for document history management.
- Enhanced JSON diffing capabilities for better patching of document history.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor: sync document history schema and simplify history service

- Sync simplified document_history table from feat/document-history-db

- Remove version/storage_kind/payload/base_version, use editor_data + saved_at

- Rewrite pagination with composite (savedAt, id) cursor

- Update TRPC APIs from version-based to historyId-based

- Replace DocumentVersionControl with AutoSaveHint

- Add integration tests for history service

*  feat: add per-source document history retention limits

- autosave / manual: retain 20 entries each

- restore / system: retain 5 entries each

- trimHistoryBySource now deletes in batches of 100 to avoid unbounded overflow

- removed obsolete constants: PATCH_THRESHOLD, RETENTION_LIMIT, SNAPSHOT_INTERVAL

- added integration tests for large overflow trimming

*  add llm_call history source and queue-based snapshot for page agent

* 💄 restyle document history list to Notion timeline

* 💄 fix history timeline alignment, unify fonts and highlight current

*  feat(PageEditor): refine document history compare UI and date formatting

Made-with: Cursor

*  feat(editor): add validation for editor data and update related interfaces

- Introduced `isValidEditorData` function to validate editor data structure.
- Updated `GetHistoryItemOutput` and `DocumentHistoryItemResult` interfaces to allow `editorData` to be `null`.
- Modified `getDocumentEditorData` to return `null` for invalid editor data.
- Added integration tests to ensure proper handling of invalid editor data in document history service.
- Enhanced editor actions to prevent saving of invalid editor data.

Signed-off-by: Innei <tukon479@gmail.com>

* 💾 chore(database): split document history indexes

* Fix manual saves and optimize history item rendering

* 🌐 locale: add missing llm_call translation key in en-US file.json

Add pageEditor.history.saveSource.llm_call = \"AI Edit\" to match
the default locale and prevent raw i18n key from showing in the
history panel.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-16 23:24:28 +08:00
CanisMinor 29734eec23 📝 docs: update release changelog skills (#13899)
docs: add release changelog skills
2026-04-16 23:14:00 +08:00
Arvin Xu c046d042f5 feat: associate web crawl documents with agent documents (#13893)
*  feat: associate web crawl documents with agent documents

- Add `associate` method to AgentDocumentModel for linking existing documents
- Add `associateDocument` to AgentDocumentsService, TRPC router, and client service
- Update web browsing executor to associate crawled pages with agent after notebook save
- Add server-side crawl-to-agent-document persistence in webBrowsing runtime
- Add `findOrCreateFolder` to DocumentModel for folder hierarchy support
- Extract `DOCUMENT_FOLDER_TYPE` constant from hardcoded 'custom/folder' strings
- Add tests for associate, findOrCreateFolder, and service layer

Fixes LOBE-7242

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

* 🐛 fix: log errors in web crawl agent document association

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

* ♻️ refactor: add onCrawlComplete callback to WebBrowsingExecutionRuntime

Replace monkey-patching of crawlMultiPages with a proper onCrawlComplete
callback in the runtime constructor options.

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

* ♻️ refactor: move document save logic into WebBrowsingExecutionRuntime

Replace onCrawlComplete callback with documentService dependency injection.
The runtime now directly handles createDocument + associateDocument internally.

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

* ♻️ refactor: pass per-call context to documentService via crawlMultiPages

Add WebBrowsingDocumentContext (topicId, agentId) as a parameter to
crawlMultiPages, which flows through to documentService methods. This
allows a singleton runtime with per-call context on the client side.

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

* 🐛 fix: enforce document ownership in associate and match root folders by null parentId

- associate: verify documentId belongs to current user before creating link
- findOrCreateFolder: add parentId IS NULL condition for root-level lookup

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:11:21 +08:00
Neko 13d1b011b7 🐛 fix(app): include working panel into Lab feature, minor fixes (#13889)
* 🐛 fix(app): include working panel into Lab feature, minor fixes

* 🐛 fix(app): conditional disabled.
2026-04-16 23:05:33 +08:00
CanisMinor 549735be7f 📝 docs: Update changelog docs and release skills (#13897)
* 🔨 chore: update .vscode/settings.json (#13894)

* 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)

Made-with: Cursor

* 📝 docs: Update changelog docs and release skills (#13897)

- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor

---------

Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
2026-04-16 22:24:48 +08:00
CanisMinor df524103e4 📝 docs: Update changelog docs and release skills (#13897)
- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor
2026-04-16 22:22:35 +08:00
Innei e487bcd8a1 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)
Made-with: Cursor
2026-04-16 22:09:38 +08:00
YuTengjing dfc6000ecd 🔨 chore: update .vscode/settings.json (#13894) 2026-04-16 21:07:05 +08:00
lobehubbot 282415b886 🔖 chore(release): release version v2.1.50 [skip ci] 2026-04-16 11:29:10 +00:00
lobehubbot 94b6827580 Merge remote-tracking branch 'origin/main' into canary 2026-04-16 11:27:19 +00:00
Innei c1174d3eb8 👷 build(database): add document history schema (#13789)
#### 💻 Change Type

- [ ]  feat
- [ ] 🐛 fix
- [ ] ♻️ refactor
- [ ] 💄 style
- [x] 👷 build
- [ ] ️ perf
- [ ]  test
- [ ] 📝 docs
- [ ] 🔨 chore

#### 🔗 Related Issue

- None

#### 🔀 Description of Change

- Extract the document history database changes from the feature branch
onto a branch based on main.
- Add the document history migration, schema, relations, model, and
database tests only.
- Exclude UI, router, and service-layer changes so the PR stays focused
on the database layer.

#### 🧪 How to Test

- Run: cd packages/database && bunx vitest run --silent=passed-only
src/models/__tests__/document.test.ts
src/models/__tests__/documentHistory.test.ts
- [x] Tested locally
- [x] Added or updated tests
- [ ] No tests needed

#### 📸 Screenshots / Videos

| Before | After |
| ------ | ----- |
| N/A | N/A |

#### 📝 Additional Information

- This PR intentionally targets main because the database migration
needs to land on the release branch first.
2026-04-16 19:26:37 +08:00
Arvin Xu cb4ad01135 🐛 fix: fix minify cli (#13888)
* update

* update

* 🔧 chore: update CLI build command in electron-builder and ensure proper newline in package.json

* Changed the CLI build command from 'npm run build' to 'npm run build:cli' in electron-builder.mjs.
* Added a newline at the end of package.json for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Innei <tukon479@gmail.com>
2026-04-16 18:39:18 +08:00
Rdmclin2 2cfcd4a75f 🔨 chore: add ENABLE_BOT_IN_DEV swtich (#13883)
* chore: add  ENABLE_BOT_IN_DEV swtich

* chore: add explicit judge
2026-04-16 17:47:31 +08:00
LiJian 843248fb77 🐛 fix: add some lost lobe-kb builtin tools (#13876)
* feat: add some lost lobe-kb builtin tools

* feat: add the list files and get file detail

* feat: add the list files and get file detail

* fix: update the search limit
2026-04-16 17:08:22 +08:00
Arvin Xu 1476cd86ee ♻️ refactor: add backgroundColor to TaskParticipant and rename name to title (#13877)
* ♻️ refactor: add backgroundColor to TaskParticipant and rename name to title

Add backgroundColor field and rename name→title in TaskParticipant interface
to match agent avatar data. Add LobeAI fallback for inbox agent in
getAgentAvatarsByIds when avatar/title are missing.
2026-04-16 17:06:51 +08:00
Innei 7c8f721d6d 💾 chore(database): sync document history schema indexes 2026-04-16 16:48:15 +08:00
Rdmclin2 85227cf467 🐛 fix: recent delete (#13878)
* chore: update skills dir

* chore: remove unused recent fetch actions and components

* fix: recent delete functions

* chore: update comments
2026-04-16 16:42:50 +08:00
Innei d526b40b78 🐛 fix(deps): pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg
Picked from canary commit 9f61b58a29.
- Bump @react-pdf/renderer from ^4.3.2 to 4.4.1
- Pin @react-pdf/image to 3.0.4 via pnpm.overrides
2026-04-16 15:01:49 +08:00
Innei a7339bea13 🌐 chore(locale): update page editor placeholder to new AI command prompt (#13872)
Update `pageEditor.editorPlaceholder` from `Start writing your page. Press / to open the command menu` to `Press "/" for AI and commands.` across all supported locales and the default locale source.
2026-04-16 14:41:07 +08:00
Arvin Xu ab05020f62 🐛 fix: default execAgent approval mode to headless (#13873)
* 🐛 fix: default execAgent approval mode to headless

Backend execAgent calls should run headlessly by default since only
frontend scenarios require manual human approval. This prevents cron
jobs and other server-side triggers from unexpectedly waiting for
human intervention.

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

*  test: add regression test for headless approval default

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:05:53 +08:00
Arvin Xu 4203e32dc7 ♻️ refactor: createAgent uses agentModel.create directly (#13871)
* ♻️ refactor: createAgent uses agentModel.create directly

The createAgent router was still going through sessionModel.create,
which is a legacy path that doesn't pass all agent fields (like
agencyConfig) to the agents table. Switch to agentModel.create
which directly inserts into the agents table with full field support.

- Add CreateAgentSchema in types package for proper input validation
- Remove dependency on insertAgentSchema from database package
- Remove sessionId from CreateAgentResult

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

* 🏷️ chore: mark session-based agent creation as deprecated

Add @deprecated JSDoc tags to the legacy session-based agent creation
path (session router, SessionService, SessionModel.create, session store,
insertAgentSchema). New code should use agent.createAgent / agentModel.create
directly.

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

* 🐛 fix: honor groupId when creating agents

Pass input.groupId as sessionGroupId to agentModel.create so that
agents created from a sidebar folder are correctly assigned to that group.

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

* 🐛 fix: resolve type errors from createAgent refactor

- Remove sessionId fallback in AddAgent.tsx and ForkAndChat.tsx
- Use z.custom<T>() for agencyConfig and tts in CreateAgentSchema
  to match agentModel.create parameter types

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:10:13 +08:00
LobeHub Bot 9583de88e3 🌐 chore: translate non-English comments to English in desktop-controller-tests (#13867)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:02:18 +08:00
LiJian 0699a0b5ce 🐛 fix: improve the skill execution error body back (#13868)
fix: improve the skill execution error body back
2026-04-16 11:43:01 +08:00
Arvin Xu dd81642d83 ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package (#13866)
* ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package

Move the Agent Gateway WebSocket client from src/libs/agent-stream/ into
a standalone workspace package at packages/agent-gateway-client/. This
eliminates the duplicate AgentStreamEvent type in apps/cli and provides
a single source of truth for the Gateway WS protocol types shared by
SPA, server, and CLI consumers.

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

* add agent-gateway-client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:25:32 +08:00
Arvin Xu f6c70210f2 ♻️ refactor(chat): remove reject-only button, unify to rejected_continue (#13865)
* ♻️ refactor(chat): remove reject-only button, unify to rejected_continue

Server-side `decision='rejected'` and `decision='rejected_continue'`
share the exact same code path — both surface the rejection to the
LLM as user feedback. Having a separate "reject only" button added UI
complexity without behavioural difference.

- Remove the "仅拒绝" button from InterventionBar popover; the single
  "拒绝" button now calls `rejectAndContinueToolCall` directly
- `rejectToolCalling` Gateway branch sends `rejected_continue` instead
  of `rejected` so all rejection paths use one decision value

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

* Update ApprovalActions.tsx

*  feat(tool): add executors field to BuiltinToolManifest and dispatch page-agent to client

Add `executors?: ('client' | 'server')[]` to `BuiltinToolManifest` so
each builtin tool declares where it can run. The server-side dispatch
logic in `aiAgent/index.ts` now reads this field instead of hardcoding
per-identifier checks.

- `lobe-local-system`: `executors: ['client', 'server']` — runs on
  client via Electron IPC or server via Remote Device proxy
- `lobe-page-agent`: `executors: ['client']` — requires EditorRuntime,
  client-only
- Stdio MCP plugins still use the `customParams.mcp.type` heuristic
  (not manifest-driven)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:45:17 +08:00
Arvin Xu 8109bbbbc3 🐛 fix(gateway): route approve/reject via lab flag (#13863)
🐛 fix(gateway): route approve/reject via lab flag, not transient server op state

After the coordinator fix for `waiting_for_human` (#13860) the paused
`execServerAgentRuntime` op is marked `completed` client-side as soon
as the server emits `agent_runtime_end`. `startOperation` then runs
`cleanupCompletedOperations(30_000)`, which deletes any op completed
more than 30 seconds ago — so by the time the user sees the
InterventionBar and clicks approve/reject, the running (or recently
completed) server op is gone.

The previous `#hasRunningServerOp` check therefore kept returning
false against a live Gateway backend, flipping approve/reject into
the client-mode `internal_execAgentRuntime` branch and stranding the
server-side paused conversation.

Switch the helper to `#shouldUseGatewayResume`, which checks the same
`isGatewayModeEnabled()` lab flag used to route the initial send. The
signal now mirrors how the conversation was dispatched and survives
the op-cleanup window.

New regression test exercises the post-coordinator-fix state: the
paused `execServerAgentRuntime` op is explicitly `completed` before
the approve call runs, and we still expect the Gateway branch to
fire with `decision='approved'`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:26:22 +08:00
Arvin Xu 1005f442d6 🐛 fix(gateway): clean up paused server op after human approve/reject (#13860)
* 🐛 fix(gateway): clean up paused server op after human approve/reject

In Gateway mode with userInterventionConfig.approvalMode='ask', the
paused execServerAgentRuntime op was never released — the loading
spinner kept spinning after the user approved, rejected, or
reject-and-continued, and reject-only silently did nothing on the
server.

- ToolAction.rejectToolCall now delegates to chatStore.rejectToolCalling
  so the Gateway resume op actually fires with decision='rejected';
  previously it only mutated local intervention state and the server's
  paused op waited forever.
- AgentRuntimeCoordinator treats waiting_for_human as end-of-stream so
  the coordinator emits agent_runtime_end when request_human_approve
  flips state, letting the client close the paused op via the normal
  terminal-event path.
- conversationControl adds #completeRunningServerOps as a fallback
  guard in the approve/reject/reject-continue Gateway branches — if
  the server-side signal is delayed or missing, the client still clears
  the orphan op before starting the resume op.

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

* 🐛 fix(gateway): defer paused-op cleanup until resume starts successfully

If `executeGatewayAgent` failed (transient network/auth/server error),
the paused `execServerAgentRuntime` op was already marked completed
locally by the pre-call `#completeRunningServerOps`. Retries would
then see no running server op, miss `#hasRunningServerOp`, and fall
through to the non-Gateway client-mode path — while the backend was
still paused awaiting human input.

Snapshot the paused op IDs before the resume call and retire them
only inside the try block after `executeGatewayAgent` resolves. On
failure the running marker stays intact so a retry still lands on
the Gateway branch and can re-issue the resume.

The helper was renamed from `#completeRunningServerOps(context)` to
`#completeOpsById(ids)` to reflect the new contract: callers must
snapshot beforehand, not re-query at completion time (which would
incorrectly match the new resume op too).

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

* 🐛 fix(gateway): avoid double reject dispatch in reject-and-continue

Now that `rejectToolCall` delegates to `chatStore.rejectToolCalling`,
the chained `await get().rejectToolCall(...)` inside
`rejectAndContinueToolCall` fired a full halting reject before the
continue call. In Gateway mode that meant two resume ops on the same
tool_call_id (`decision='rejected'` followed by
`decision='rejected_continue'`) racing server-side; in client mode it
duplicated reject bookkeeping that `chatStore.rejectAndContinueToolCalling`
already handles internally.

Drop the chained call and fire `onToolRejected` inline so hook
semantics are preserved. `chatStore.rejectAndContinueToolCalling` is
now the single entry point for both the rejection persist and the
continue dispatch.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:43:00 +08:00
Arvin Xu 2cf65e9fb3 💄 style: agent documents (#13857)
* improve style

* improve style
2026-04-16 01:05:27 +08:00
Arvin Xu 6636b35188 🐛 fix: drop manifests missing api before feeding ToolsEngine (#13856)
🐛 fix(toolEngineering): drop manifests missing `api` before feeding ToolsEngine

`ToolsEngine.convertManifestsToTools` calls `manifest.api.map(...)`
without a null check, so any manifest that is truthy but lacks a valid
`api` array crashes the entire tools build with "Cannot read properties
of undefined (reading 'map')". This takes down anything that touches
the tools pipeline on that agent — including TokenTag in ChatInput,
which is why users see the crash on the chat page load path.

Manifests are merged from 5 sources (installed plugins, builtin tools,
Klavis, LobeHub skills, caller-supplied extras), only some of which
filter falsy entries, and none validate `api`. Guard defensively at
the merge point and log the offending source + identifier so the
underlying bad data can be traced.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:04:55 +08:00
Arvin Xu 8475bc11fc 🐛 fix(builtin-tool-gtd): add server runtime for GTD tool (#13854)
*  feat(builtin-tool-gtd): add server runtime for GTD tool

Implement server-side execution runtime so the GTD tool works when
agents run in a pure server context (bot platforms, async task workers,
QStash workflows). Previously only the client executor existed, which
relied on `useNotebookStore` and `notebookService` and would break on
the server.

- `packages/builtin-tool-gtd/src/ExecutionRuntime/index.ts`: pure
  `GTDExecutionRuntime` class with an injected service interface,
  covering createPlan/updatePlan/createTodos/updateTodos/clearTodos
  and execTask/execTasks. Since server runtime has no stepContext,
  todo state is read from / written back to the Plan document's
  `metadata.todos` field.
- `src/server/services/toolExecution/serverRuntimes/gtd.ts`: factory
  wiring `DocumentModel` + `TopicDocumentModel` into the runtime and
  registering under `GTDIdentifier`.

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

* ♻️ refactor(builtin-tool-gtd): share runtime logic between executor and server

Make the client executor a thin adapter over `GTDExecutionRuntime` so
all processing logic (todo reducer, plan CRUD flow, execTask state
builder, output formatting) lives in one place. Previously the server
runtime was a near-duplicate of the client executor.

- Expand `GTDRuntimeContext` with `currentTodos`, `messageId`, `signal`
  so both callers can thread their environment through:
  - client supplies `currentTodos` from stepContext / pluginState via
    `getTodosFromContext`, and `messageId` for execTask parentMessageId
  - server lets the runtime resolve todos from the plan document's
    metadata when `currentTodos` is not supplied
- Split service surface into `updatePlan` (user-facing: goal / desc /
  context — client routes through `useNotebookStore` to refresh SWR)
  vs `updatePlanMetadata` (silent todos sync — client stays on the
  raw `notebookService`)
- Runtime methods now return `BuiltinToolResult` (superset of
  `BuiltinServerRuntimeOutput`), so `stop: true` on execTask /
  execTasks is typed cleanly without `@ts-expect-error`

Net effect: `executor/index.ts` shrinks from 510 → 134 lines; the
server factory just maps models to the service interface.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:04:48 +08:00
LobeHub Bot 3bb4fd6046 🌐 chore: translate non-English comments to English in lambda-router-tests (#13838)
🌐 chore: translate non-English comments to English in lambda router tests

Translated all Chinese/CJK comments to English in 6 test files under
src/server/routers/lambda/__tests__/. Code logic and string literals
are unchanged; only explanatory comments were translated.

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 23:59:08 +08:00
Arvin Xu 9608494b0a 💄 style(chat): tighten execServerAgentRuntime loading copy (#13855)
💄 style(chat): tighten `execServerAgentRuntime` loading copy

Current text was trying to do too much in one line — status + two
separate user affordances — and read as an explanation, not a status.
Replaces it with a status-first line that mentions where the work is
happening and the single reassurance users actually need.

- EN: "Task is running in the server. You are safe to leave this page."
- zh-CN: "任务正在服务器运行,您可以放心离开此页面。"

Only en-US and zh-CN are edited; CI translates the rest from the
default file.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:42:37 +08:00
Innei bc7b798dbb 🐛 fix(conversation): improve workflow display when user intervention is pending (#13847)
* 🐛 fix(conversation): improve workflow display when user intervention is pending

Made-with: Cursor

* 🐛 fix(builtin-tool-activator): add ActivatedToolInfo typing to requestedTools for tsgo compat

requestedTools was inferred as `{ identifier, name }[]` which lacks the
`avatar` property required by `ActivatedToolInfo`, causing tsgo errors.
2026-04-15 23:30:34 +08:00
Arvin Xu 986bd2f7ec 🐛 fix(agent-runtime): fetch tool plugin from message_plugins for resumeApproval (#13852)
`messageModel.findById(parentMessageId)` only returns the row from the
`messages` table — the tool-call metadata (identifier / apiName /
arguments / type / toolCallId) lives in the separate `message_plugins`
table. The resumeApproval path was reading `(resumeParentMessage as any).plugin`
and `(resumeParentMessage as any).tool_call_id`, both always undefined,
which meant:

- Approved tool calls were dispatched with `identifier: undefined`,
  causing the server-side tool executor to throw
  `Builtin tool "undefined" is not implemented`. The follow-up LLM
  step could still describe success (it sees the user prompt + picks
  plausible output) but the tool message content is permanently the
  error string.
- The toolCallId mismatch guard was silently disabled because the
  stored value was always null → validation always passed regardless
  of what the client sent.

Fix: query `messagePlugins.findFirst` by message id, use the fetched
row for both the toolCallId equality check and the approvedToolCall
payload that the runtime dispatches.

Tests:
- Mock `db.query.messagePlugins.findFirst` with the plugin fields so
  existing asserts on `approvedToolCall.identifier`/`apiName` pass
  against real values.
- Move `tool_call_id` / identifier / apiName / arguments / type out of
  the mock `messages` row fixture into a separate `pendingToolPlugin`
  fixture that mirrors the actual DB layout.
- Flip the "toolCallId mismatch" guard test to mutate the plugin mock
  (not the message mock) — this is exactly the class of bug the fetch
  guards against, so the test would have masked it before.
- New guard test: throw when `messagePlugins.findFirst` returns
  undefined (stale message id, wrong user, etc.).

Discovered during E2E verification of LOBE-7152 approve flow — the
approve decision was flipping to the new op correctly but every tool
execution was failing with the "undefined" error.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:53:50 +08:00
Arvin Xu 843cb8f30b 🔨 chore: wire server-mode human approval through conversationControl (#13830)
 feat(chat): server-mode human approval via new Gateway op + resumeApproval

When the current agent runtime is Gateway-mode (execServerAgentRuntime),
approve / reject / reject_continue now start a **new** Gateway op carrying
a `resumeApproval` decision instead of resuming the paused op in place
over tRPC — mirroring the "interrupt + new op" pattern from LOBE-7142
(stop/interrupt). This sidesteps the stepIndex / executeStep early-exit
race that was blocking the in-place resume path and matches the Linear
spec for LOBE-7152. Client mode is unchanged.

### Client

- `conversationControl.ts`
  - `approveToolCalling` / `rejectToolCalling` / `rejectAndContinueToolCalling`:
    server-mode branch calls `executeGatewayAgent({ message: '',
    parentMessageId: toolMessageId, resumeApproval: { decision, ... } })`.
    The local runtime never spins up; the new op's `agent_runtime_end`
    clears loading.
  - `#hasRunningServerOp` replaces the old `#getServerOperationId` helper
    (we no longer need the paused op's id). Forwards scope/groupId/
    subAgentId from `ConversationContext` into the operation lookup so
    group/thread conversations correctly resolve their running server op
    — `operationsByContext` is keyed on the full `messageMapKey`.
- `gateway.ts` — `executeGatewayAgent` takes an optional `resumeApproval`
  and forwards it to `aiAgentService.execAgentTask`.
- `services/aiAgent.ts` — `ExecAgentTaskParams.resumeApproval` with new
  `ResumeApprovalParam` shape (decision + parentMessageId + toolCallId
  + optional rejectionReason).
- `gatewayEventHandler.ts` — kept the `toolMessageIds` branch that fetches
  pending tool messages on `tools_calling`.
- `services/agentRuntime/{type,index}.ts` — removed the short-lived
  `toolMessageId` / `reject_continue` additions; this flow no longer
  routes through `processHumanIntervention`.
- `store/chat/slices/operation/selectors.ts` — `getOperationsByContext` /
  `hasRunningOperationByContext` now take `MessageMapKeyInput` so scope/
  group/subAgent fields are honoured end-to-end.

### Server

- `ExecAgentSchema` / `InternalExecAgentParams.resumeApproval` — optional
  `{ decision, parentMessageId, rejectionReason?, toolCallId }`.
- `AiAgentService.execAgent`
  - `resumeApproval` implies resume semantics (skip user-message creation,
    reuse `parentMessageId` as the target tool message). Folded into a
    single `effectiveResume` flag so the existing resume branches apply.
  - Validates parent is a `role='tool'` message whose `tool_call_id`
    matches the request — guards stale / double-clicks.
  - Writes the decision to DB before `historyMessages` is fetched so the
    runtime sees the updated tool message on the first step:
    * `approved` → `intervention: { status: 'approved' }`
    * `rejected` / `rejected_continue` → tool content =
      "User reject this tool calling [with reason: X]",
      `intervention: { status: 'rejected', rejectedReason }`.
  - Branches initial runtime context:
    * `approved` → `phase: 'human_approved_tool'` + `approvedToolCall`
      payload rebuilt from the tool message plugin → runtime executes
      the tool.
    * `rejected` / `rejected_continue` → `phase: 'user_input'` with
      empty content → LLM re-reads history (now including the rejected
      tool) and responds. Both decisions share this path: the client
      split is only about optimistic writes and button UX; once the
      rejection is persisted there's nothing meaningful to differentiate
      server-side.

### Tests

- `conversationControl.test.ts` — rewrote the three server-mode blocks
  to spy `executeGatewayAgent` and assert the `resumeApproval` payload
  shape. Added a regression test covering group-scope lookup so dropping
  scope/groupId from `#hasRunningServerOp` breaks the suite.
- `execAgent.resumeApproval.test.ts` (new) — covers approved and the
  unified rejected branches (parameterized), the no-reason fallback, and
  the role/tool_call_id validation guards.

Relates to LOBE-7152.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:17:22 +08:00
Arvin Xu 75626de0b3 🐛 fix: forward serverUrl in WS auth for apiKey verification (#13824)
* 🐛 fix: forward serverUrl in WS auth for apiKey verification

The agent gateway verifies an apiKey by calling
\`\${serverUrl}/api/v1/users/me\` with the token, so \`serverUrl\` has to be
part of the WebSocket auth handshake. The device-gateway-client already
does this; \`lh agent run\` was missing it, producing
"Gateway auth failed: Missing serverUrl for apiKey auth".

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

* 🔨 chore: bump cli to 0.0.7

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:16:11 +08:00
Innei ad634daf32 🧹 chore(onboarding): remove builtin-agent-onboarding and consolidate prompts (#13825)
* 🧹 chore: remove builtin-agent-onboarding and consolidate web onboarding

- Merge agent system role into builtin-agents; colocate toolSystemPrompt in builtin-tool-web-onboarding
- Drop unused QuestionRenderer client bundle
- Gate onboarding footer switch/skip on AGENT_ONBOARDING_ENABLED for agent route

Made-with: Cursor

* 🧪 test: fix onboarding layout translation mock

* 🧪 test: align onboarding layout test with feature flag

* 🧪 test: type onboarding business const mock
2026-04-15 20:40:37 +08:00
Arvin Xu f99935e992 🐛 fix(agent-runtime): carry persisted assistant id into state.messages (#13841)
When `call_llm` pushed the assistant turn into `state.messages`, it
dropped the DB id even though the row was already persisted. The
downstream `request_human_approve` executor filters parent lookup on
`m.role === 'assistant' && m.id`, and the DB fallback query is not
reliably finding the just-written row on every topology — so when
human-approve fires on the fresh LLM turn the op errors out with
"No assistant message found as parent for pending tool messages".

Attach `assistantMessageItem.id` to the pushed message so the existing
in-memory lookup hits, and nextContext's `parentMessageId` and
`state.messages` agree on a single source of truth.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:02:40 +08:00
Neko 632a6383f0 feat(app): working panel, and many agent document feat (#13766) 2026-04-15 19:18:24 +08:00
LiJian 15fcce97c9 ♻️ refactor: add more tools in lobe-agent-manangerment(modify、update、delete) (#13842)
* feat: add more tools in lobe-agent-manangerment

* feat: add the ensureAgentLoaded to modify it

* feat: add the update prompt tools
2026-04-15 17:57:05 +08:00
Neko e5be1801a1 🐛 fix(userMemories,database): bm25 should escape more characters like < and > (#13843) 2026-04-15 17:30:32 +08:00
Innei 64fc6d4bbd feat(database): add document history table and update related models
- Introduced a new `document_histories` table to track changes made to documents, including fields for `editor_data`, `save_source`, and `saved_at`.
- Updated foreign key relationships to link `document_histories` with `documents` and `users`.
- Modified existing models and tests to accommodate the new document history functionality, including changes to pagination and retrieval methods.
- Removed the versioning system from documents in favor of a more flexible history tracking approach.

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-15 14:39:37 +08:00
LiJian 524e07540c 🐛 fix: update the builtin lobehub skill manifest (#13840)
* fix: update the lobehub skill manifest

* fix: remove the no use code

* fix: update the test
2026-04-15 13:24:57 +08:00
Arvin Xu 9f61b58a29 feat(agent-runtime): server-side human approval flow (#13829)
*  feat(agent-runtime): implement server-side human approval flow

Port the client-mode human approval executors (request_human_approve,
call_tool resumption, handleHumanIntervention) to the server agent
runtime so that execServerAgentRuntime can correctly pause on
waiting_for_human and resume on approve / reject / reject_continue.

- request_human_approve now creates one `role='tool'` message per pending
  tool call with `pluginIntervention: { status: 'pending' }` and ships
  the `{ toolCallId → toolMessageId }` mapping on the `tools_calling`
  stream chunk.
- call_tool gains a `skipCreateToolMessage` branch that updates the
  pre-existing tool message in-place (prevents duplicate rows / parent_id
  FK violations that show up as LOBE-7154 errors).
- AgentRuntimeService.handleHumanIntervention implements all three
  paths: approve → `phase: 'human_approved_tool'`; reject → interrupted
  with `reason: 'human_rejected'`; reject_continue → `phase: 'user_input'`.
- ProcessHumanIntervention schema carries `toolMessageId` and a new
  `reject_continue` action; schema remains permissive (handler no-ops on
  missing toolMessageId) to keep legacy callers working.

Fixes LOBE-7151

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

* 🐛 fix(agent-runtime): address LOBE-7151 review (P1 reject_continue, P2 duplicate tool msg)

P1 — reject_continue with remaining pending tools must NOT resume the LLM.
Previously `handleHumanIntervention` kept `status='waiting_for_human'` but
returned `nextContext: { phase: 'user_input' }`, which `executeStep` would
hand to `runtime.step` immediately, breaking batch semantics. Now when
other tools are still pending, the rejection is persisted but no context
is returned; the `user_input` continuation only fires when this is the
last pending tool.

P2 — request_human_approve was pushing an empty placeholder
`{ role: 'tool', tool_call_id, content: '' }` into `newState.messages`
to "reflect" the newly-created pending DB row. On resume, the `call_tool`
skip-create path appends the real tool result, leaving two entries for
the same `tool_call_id` in runtime state. The downstream short-circuit
(`phase=human_approved_tool` → `call_tool`) doesn't consult
state.messages, so the placeholder was unused cost. Removed.

Also fixes a TS 2339 in the skipCreateToolMessage test where
`nextContext.payload` is typed `{}` and needed an explicit cast.

Tests: 99 pass (82 RuntimeExecutors + 17 handleHumanIntervention), type-check clean.
Verified end-to-end via the human-approval eval — it now exercises a
multi-turn retry path (LLM calls the gated tool twice) and both
approvals resolve cleanly through to `completionReason=done`.

Relates to LOBE-7151

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

* pin @react-pdf/renderer

* 🐛 fix(deps): pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg

@react-pdf/image@3.1.0 (auto-resolved via layout@4.6.0 ← renderer@4.4.1)
declares `@react-pdf/svg@^1.1.0` as a dependency, but the svg package was
unpublished/made private on npm (returns 404). CI installs blow up with
ERR_PNPM_FETCH_404.

Upstream issue: https://github.com/diegomura/react-pdf/issues/3377

Pin image to 3.0.4 (the last release before the broken svg dep was
introduced) via pnpm.overrides until react-pdf publishes a fix.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:07:06 +08:00
Arvin Xu f12cf8f2ea 🐛 fix: fail fast when tool/assistant message persist hits a missing parent (#13828)
* 🐛 fix: fail fast when tool/assistant message persist hits a missing parent

When a conversation parent was deleted mid-operation (LOBE-7154), the
runtime was silently swallowing the parent_id FK violation in three tool
persist paths and continuing with a stale parentMessageId. The next LLM
call hit the same FK without context, surfacing as a raw SQL error to
the user after burning several LLM + tool call round trips.

Changes

- packages/types: add AgentRuntimeErrorType.ConversationParentMissing
- new messagePersistErrors.ts helper: FK detection + structured error
  constructor + persist-fatal marker (keeps RuntimeExecutors smaller)
- RuntimeExecutors:
  - call_tool: publish error event + re-throw on persist failure;
    outer catch propagates when persist-fatal
  - call_tools_batch: same, mark so the per-tool outer catch doesn't
    swallow and fall back to the already-deleted parent
  - resolve_aborted_tools: same pattern
  - call_llm: preflight parent existence via findById so we fail before
    the LLM call instead of after
- tests: replace old swallow-on-fail expectations, add LOBE-7158 cases
  for each executor plus focused unit tests for the helper module

Fixes LOBE-7158

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

* 💄 chore: publish normalized ConversationParentMissing on persist failure

Review feedback on LOBE-7158: the three persist catches were emitting
the raw DB exception as a stream `error` event before normalizing it.
Clients treat `error` events as terminal and surface `event.data.error`
directly, so the raw SQL text leaked to users and ended the stream
before the typed `ConversationParentMissing` throw could propagate.

Move normalization ahead of the publish in call_tool, call_tools_batch,
and resolve_aborted_tools so the stream event always carries the
intended business error. Add a regression assertion on the
call_tool FK test that the error event's `errorType` is
`ConversationParentMissing` and no `Failed query` text leaks through.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:27:01 +08:00
Arvin Xu 1a98e1b5aa 💄 style(nav-panel): remove nav panel content switch animation (#13814)
Drop the `motion/react` slide + fade transition on NavPanel content
switches (e.g. navigating from `/` to `/agent`). The new content now
renders directly without the 0.28s x-translate animation.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:10:37 +08:00
Innei b4fc85b57b 💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle (#13821)
* 🔖 chore(release): release version v2.1.49 [skip ci]

* 💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle

- Split enableProxy into instant-apply (no save required)
- Floating pill SaveBar fixed bottom-center, visible only when dirty
- Test connection feedback moved to toast (@lobehub/ui)
- Refresh style guidance: prefer createStaticStyles + cssVar

Fixes LOBE-7071

* 🐛 fix(proxy-settings): rollback enable toggle on save failure, preserve in-progress edits

---------

Co-authored-by: lobehubbot <i@lobehub.com>
2026-04-15 00:05:00 +08:00
Rdmclin2 fd0d846975 feat: support layout custom sort and fix copy (#13812)
* fix: menu locale keys

* feat: support resort sidebar

* feat: add lock to middle messages

* feat: add memory menu and default hidden

* fix: lint error

* fix: legacy secion order

* chore: add test cases

* chore: remove top zone

* feat: custom sidebar reorder

* chore: fix sidebar items
2026-04-14 23:49:47 +08:00
Arvin Xu 41efd16bba 🔨 chore: update cli version (#13822)
update cli version
2026-04-14 23:37:28 +08:00
Innei b9c4b87a90 🩹 fix(database): enforce document history ownership and pagination 2026-04-14 00:51:13 +08:00
Innei e3eef044ca 👷 build(database): add document history schema 2026-04-14 00:05:06 +08:00
lobehubbot d47f4fec76 🔖 chore(release): release version v2.1.49 [skip ci] 2026-04-10 09:51:03 +00:00
1214 changed files with 81139 additions and 10371 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ description: 'Code review checklist for LobeHub. Use when reviewing PRs, diffs,
- Newly written code duplicates existing utilities in `packages/utils` or shared modules?
- Copy-pasted blocks with slight variation — extract into shared function
- `antd` imports replaceable with `@lobehub/ui` wrapped components (`Input`, `Button`, `Modal`, `Avatar`, etc.)
- Use `antd-style` token system, not hardcoded colors
- Use `antd-style` token system, not hardcoded colors; prefer `createStaticStyles` + `cssVar.*` over `createStyles` + `token` unless runtime computation is required
### Database
+155
View File
@@ -0,0 +1,155 @@
---
name: docs-changelog
description: 'Writing guide for website changelog pages under docs/changelog/*.mdx. Use when creating or editing product update posts in EN/ZH. Not for GitHub Release notes.'
---
# Docs Changelog Writing Guide
## Scope Boundary (Important)
This skill is only for changelog pages in:
- `docs/changelog/*.mdx`
This skill is **not** for GitHub Releases.\
If the user asks for release PR body / GitHub Release notes, load `../version-release/SKILL.md`.
## Mandatory Companion Skills
For every docs changelog task, you MUST load:
- `../microcopy/SKILL.md`
- `../i18n/SKILL.md` (when EN/ZH pair is involved)
## File and Naming Convention
Use date-based file names:
- English: `docs/changelog/YYYY-MM-DD-topic.mdx`
- Chinese: `docs/changelog/YYYY-MM-DD-topic.zh-CN.mdx`
EN and ZH files must exist as a pair and describe the same release facts.
## Frontmatter Requirements
Each file should include:
```md
---
title: <Title>
description: <1 sentence summary>
tags:
- <Tag 1>
- <Tag 2>
---
```
Rules:
1. `title` should match the H1 title in meaning.
2. `description` should be concise and user-facing.
3. `tags` should be feature-oriented, not internal-team labels.
## Content Structure (Recommended)
Use this shape unless the user requests otherwise:
1. `# <Title>`
2. Opening paragraph (2-4 sentences): user-visible impact
3. 1-3 capability sections (optional `##` headings)
4. `## Improvements and fixes` / `## 体验优化与修复` with concise bullets
Keep heading count low and avoid heading-per-bullet structure.
## Writing Rules
1. Keep all claims factual and tied to actual shipped changes.
2. Explain user value first, implementation second.
3. Prefer natural narrative paragraphs over pure bullet dumps.
4. Avoid marketing exaggeration and vague adjectives.
5. Keep internal terms consistent across EN/ZH files.
6. Keep EN/ZH section order aligned and scope-aligned.
## EN/ZH Synchronization Rules
When generating bilingual changelogs:
1. Keep the same key facts in the same order.
2. Localize naturally; do not do literal sentence-by-sentence translation.
3. If one version has an `Improvements and fixes` bullet list, the other should have equivalent list intent.
4. Do not introduce capabilities in only one language unless explicitly requested.
## Length Guidance
- Small update: 3-5 short paragraphs total
- Medium update: 4-7 short paragraphs + concise fix bullets
- Large update: 6-10 short paragraphs split into 2-4 sections
Do not pad content when changes are limited.
## Authoring Workflow
1. Collect source facts from PRs/commits/issues.
2. Group changes by user workflow (not by internal module path).
3. Draft EN and ZH versions with aligned structure.
4. Verify terminology using `microcopy`/`i18n` guidance.
5. Final pass: remove AI-like filler and tighten sentences.
## Docs Changelog Template (English)
```md
---
title: <Feature title>
description: <One-sentence summary for users>
tags:
- <Tag A>
- <Tag B>
---
# <Feature title>
<Opening paragraph: what changed for users and why it matters.>
<Optional section paragraph for key capability 1.>
<Optional section paragraph for key capability 2.>
## Improvements and fixes
- <Fix or optimization 1>
- <Fix or optimization 2>
```
## Docs Changelog Template (Chinese)
```md
---
title: <功能标题>
description: <一句话说明>
tags:
- <标签 A>
- <标签 B>
---
# <功能标题>
<开场段:这次更新给用户带来的直接变化。>
<可选能力段 1。>
<可选能力段 2。>
## 体验优化与修复
- <优化或修复 1>
- <优化或修复 2>
```
## Quick Checklist
- [ ] File path matches `docs/changelog` naming convention
- [ ] EN and ZH versions both exist and match in facts
- [ ] Opening paragraph explains user-facing outcome
- [ ] Main body is narrative-first, not bullet-only
- [ ] `Improvements and fixes` section is concise and concrete
- [ ] No fabricated claims or unsupported scope
+14 -10
View File
@@ -173,6 +173,10 @@ agent-browser state save auth.json
agent-browser state load auth.json
```
### LobeHub dev server — inject better-auth cookie
`agent-browser --headed` on macOS can create an off-screen Chromium window, blocking manual login. For a local LobeHub dev server (e.g. `localhost:3011`), copy the `better-auth.session_token` cookie out of a **Network request** in the user's own Chrome DevTools and load it via `state load`. See [references/agent-browser-login.md](./references/agent-browser-login.md) for the full recipe.
## Semantic Locators (Alternative to Refs)
```bash
@@ -393,16 +397,16 @@ The pattern is the same for every platform:
Pick the file for your target platform — each contains activation, navigation, send-message, and verification snippets specific to that app:
| Platform | Reference | Quick switcher |
| ------------- | ------------------------------------------------ | -------------- |
| Discord | [reference/discord.md](./reference/discord.md) | `Cmd+K` |
| Slack | [reference/slack.md](./reference/slack.md) | `Cmd+K` |
| Telegram | [reference/telegram.md](./reference/telegram.md) | `Cmd+F` |
| WeChat / 微信 | [reference/wechat.md](./reference/wechat.md) | `Cmd+F` |
| Lark / 飞书 | [reference/lark.md](./reference/lark.md) | `Cmd+K` |
| QQ | [reference/qq.md](./reference/qq.md) | `Cmd+F` |
| Platform | Reference | Quick switcher |
| ------------- | -------------------------------------------------- | -------------- |
| Discord | [references/discord.md](./references/discord.md) | `Cmd+K` |
| Slack | [references/slack.md](./references/slack.md) | `Cmd+K` |
| Telegram | [references/telegram.md](./references/telegram.md) | `Cmd+F` |
| WeChat / 微信 | [references/wechat.md](./references/wechat.md) | `Cmd+F` |
| Lark / 飞书 | [references/lark.md](./references/lark.md) | `Cmd+K` |
| QQ | [references/qq.md](./references/qq.md) | `Cmd+F` |
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [reference/osascript-common.md](./reference/osascript-common.md). Read this first if you're new to osascript automation.
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [references/osascript-common.md](./references/osascript-common.md). Read this first if you're new to osascript automation.
---
@@ -513,4 +517,4 @@ Outputs to `.records/` directory (gitignored): `<name>.mp4` (video) + `<name>/`
### osascript
See [reference/osascript-common.md](./reference/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
See [references/osascript-common.md](./references/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
@@ -0,0 +1,110 @@
# Log `agent-browser` into a local LobeHub dev server
`agent-browser --headed` on macOS often creates the Chromium window off-screen — the user can't see or interact with it, so manual login inside the agent-browser session fails. Instead of sharing the user's real Chrome profile, copy the **better-auth session cookie** out of a request in DevTools and inject it into the agent-browser session as a Playwright-style state file.
## When to use
- You need `agent-browser` to reach an authenticated page on `http://localhost:<port>` (e.g. `localhost:3011`).
- The user already has a logged-in tab of the same dev server in their own Chrome.
- Spawning a headed Chromium to let the user log in manually is unreliable (window off-screen, no interaction).
Do **not** use this on production URLs — only local dev. Treat the cookie as a secret: don't paste it into shared logs, PRs, or commit it anywhere.
## Step 1 — Ask the user to copy the cookie from a Network request, NOT `document.cookie`
`document.cookie` will not return HttpOnly cookies, which is exactly where better-auth puts its session. Instruct the user:
1. Open the logged-in tab (`http://localhost:<port>/…`) in their own Chrome.
2. `Cmd+Option+I`**Network** tab.
3. Refresh, click any same-origin request (e.g. the top-level document request).
4. In the right pane under **Request Headers**, right-click the `Cookie:` line → **Copy value** (or copy the entire header).
5. Paste the string into chat.
You only need the better-auth pieces. Everything else (Clerk, `LOBE_LOCALE`, HMR hash, theme vars) is noise and can stay. The minimum viable set is:
```
better-auth.session_token=<value>; better-auth.state=<value>
```
## Step 2 — Build a Playwright-style state file
`agent-browser state load` expects Playwright's `storageState` format: a JSON with a `cookies` array and an `origins` array.
```bash
cat > /tmp/mkstate.py << 'PY'
import json, sys, time
# Read the Cookie header from stdin (allows optional "Cookie: " prefix).
raw = sys.stdin.read().strip()
if raw.lower().startswith("cookie:"):
raw = raw.split(":", 1)[1].strip()
# Keep only better-auth cookies. Extend this set if the app genuinely needs more.
WANTED = {"better-auth.session_token", "better-auth.state"}
cookies = []
exp = int(time.time()) + 30 * 24 * 3600 # 30 days
for pair in raw.split("; "):
if "=" not in pair:
continue
name, _, value = pair.partition("=")
if name not in WANTED:
continue
cookies.append({
"name": name,
"value": value,
"domain": "localhost",
"path": "/",
"expires": exp,
"httpOnly": False,
"secure": False,
"sameSite": "Lax",
})
if not cookies:
sys.stderr.write("no better-auth cookies found in input\n")
sys.exit(1)
print(json.dumps({"cookies": cookies, "origins": []}, indent=2))
PY
# Feed the copied Cookie header in via env var or heredoc.
printf '%s' "$COOKIE_HEADER" | python3 /tmp/mkstate.py > /tmp/state.json
```
**Note on `httpOnly`**: the real cookie in the user's browser is HttpOnly, but `storageState` doesn't enforce the flag on load — it just attaches the value. Storing with `httpOnly: false` is fine for local dev and sidesteps a CDP-context quirk where HttpOnly cookies sometimes fail to attach.
## Step 3 — Load state and navigate
```bash
SESSION="my-test" # any stable session name
agent-browser --session "$SESSION" state load /tmp/state.json
agent-browser --session "$SESSION" open "http://localhost:3011/"
agent-browser --session "$SESSION" get url
# Expect NOT /signin?callbackUrl=… — if you still see signin, cookie didn't apply.
```
## Step 4 — Verify
```bash
agent-browser --session "$SESSION" snapshot -i | head -20
# Look for the user's avatar/name in the sidebar, or absence of the signin form.
```
## Common failure modes
| Symptom | Cause | Fix |
| ----------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------- |
| Still redirects to `/signin` after `state load` | User pasted from `document.cookie` → missed HttpOnly session | Re-pull from Network request Headers, not console |
| `state load` reports 0 cookies | Separator wrong, or user pasted URL-decoded value | Keep the raw `Cookie:` header as-is; split on `"; "` |
| Login works briefly then expires | `better-auth.session_token` rotated (user logged out / signed in again) | Re-copy and re-load |
| Domain mismatch | Use `domain: "localhost"` literally, no leading dot for local dev | — |
## Scope
Only covers authenticating an **agent-browser** session into a **local** LobeHub dev server. It does not:
- Work for production — production cookies are `Secure; HttpOnly; Domain=.lobehub.com` and must be delivered over HTTPS.
- Replace real OAuth flows — tests that must exercise the login UI need a real Chromium with `--remote-debugging-port` or a bot account.
- Flow cookies back to the user's Chrome — injection is one-way (into agent-browser only).
+73 -36
View File
@@ -1,64 +1,76 @@
---
name: modal
description: Modal imperative API guide. Use when creating modal dialogs using createModal from @lobehub/ui. Triggers on modal component implementation or dialog creation tasks.
description: MUST use when creating, editing, or writing modal dialogs or imperative modals. Prefer createModal / useModalContext / confirmModal from @lobehub/ui/base-ui; root @lobehub/ui is legacy (antd Modal). Covers patterns, ModalHost, and migration notes.
user-invocable: false
---
# Modal Imperative API Guide
Use `createModal` from `@lobehub/ui` for imperative modal dialogs.
## Recommended: `@lobehub/ui/base-ui`
## Why Imperative?
New code should use the **base-ui** modal stack (headless primitives, not antd `Modal`):
| Mode | Characteristics | Recommended |
| ----------- | ------------------------------------- | ----------- |
| Declarative | Need `open` state, render `<Modal />` | ❌ |
| Imperative | Call function directly, no state | ✅ |
- `createModal`, `confirmModal`, `ModalHost` from `@lobehub/ui/base-ui`
- `useModalContext` from `@lobehub/ui/base-ui` inside modal **content**
## File Structure
Body slot: pass **`content`** (or `children`; runtime uses `content ?? children`).
### Global `ModalHost` (required)
Base-ui `createModal` renders through a **separate** host from the root package. The app must mount **`ModalHost`** from `@lobehub/ui/base-ui` once near the root (e.g. next to other global hosts). Without it, `createModal` calls will not appear.
If the project only mounts `ModalHost` from `@lobehub/ui`, add a second lazy `ModalHost` from `@lobehub/ui/base-ui` until all imperative modals are migrated.
### Why imperative?
| Mode | Characteristics | Recommended |
| ----------- | ------------------------------------ | ----------- |
| Declarative | `open` state + `<Modal />` | ❌ |
| Imperative | Call `createModal()`, no local state | ✅ |
### File structure
```
features/
└── MyFeatureModal/
├── index.tsx # Export createXxxModal
└── MyFeatureContent.tsx # Modal content
├── index.tsx # export createXxxModal
└── MyFeatureContent.tsx # modal body
```
## Implementation
### 1. Content Component (`MyFeatureContent.tsx`)
### 1. Content (`MyFeatureContent.tsx`)
```tsx
'use client';
import { useModalContext } from '@lobehub/ui';
import { useModalContext } from '@lobehub/ui/base-ui';
import { useTranslation } from 'react-i18next';
export const MyFeatureContent = () => {
const { t } = useTranslation('namespace');
const { close } = useModalContext(); // Optional: get close method
const { close } = useModalContext();
return <div>{/* Modal content */}</div>;
return <div>{/* ... */}</div>;
};
```
### 2. Export createModal (`index.tsx`)
### 2. `createModal` (`index.tsx`)
```tsx
'use client';
import { createModal } from '@lobehub/ui';
import { t } from 'i18next'; // Note: use i18next, not react-i18next
import { createModal } from '@lobehub/ui/base-ui';
import { t } from 'i18next';
import { MyFeatureContent } from './MyFeatureContent';
export const createMyFeatureModal = () =>
createModal({
allowFullscreen: true,
children: <MyFeatureContent />,
destroyOnHidden: false,
content: <MyFeatureContent />,
footer: null,
styles: { body: { overflow: 'hidden', padding: 0 } },
maskClosable: true,
styles: {
content: { overflow: 'hidden', padding: 0 },
},
title: t('myFeature.title', { ns: 'setting' }),
width: 'min(80%, 800px)',
});
@@ -76,27 +88,52 @@ const handleOpen = useCallback(() => {
return <Button onClick={handleOpen}>Open</Button>;
```
## i18n Handling
### i18n
- **Content component**: `useTranslation` hook (React context)
- **createModal params**: `import { t } from 'i18next'` (non-hook, imperative)
- **Content**: `useTranslation` in components.
- **`createModal` options**: `import { t } from 'i18next'` where hooks are unavailable.
## useModalContext Hook
### `useModalContext`
```tsx
const { close, setCanDismissByClickOutside } = useModalContext();
```
## Common Config
### Common options (base-ui)
| Property | Type | Description |
| ----------------- | ------------------- | ------------------------ |
| `allowFullscreen` | `boolean` | Allow fullscreen mode |
| `destroyOnHidden` | `boolean` | Destroy content on close |
| `footer` | `ReactNode \| null` | Footer content |
| `width` | `string \| number` | Modal width |
`ImperativeModalProps` builds on `BaseModalProps`: `title`, `width`, `maskClosable`, `open`, `onOpenChange`, `footer`, `styles` / `classNames` (keys: `backdrop`, `popup`, `header`, `title`, `close`, `content`, …).
| Property | Notes |
| -------------- | ---------------------------------------- |
| `content` | Main body (preferred name vs `children`) |
| `maskClosable` | Click outside to dismiss |
| `styles.*` | Semantic regions, not antd `styles.body` |
### Confirm
```tsx
import { confirmModal } from '@lobehub/ui/base-ui';
confirmModal({
title: '…',
content: '…',
okText: '…',
cancelText: '…',
onOk: async () => {},
});
```
---
## Legacy: `@lobehub/ui` (root)
Older call sites use **`createModal` from `@lobehub/ui`**, which is typed as **antd `Modal` props** (`children`, `allowFullscreen`, `getContainer`, `destroyOnHidden`, `styles.body`, etc.). Prefer migrating new work to **`@lobehub/ui/base-ui`**.
Examples (legacy): `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`.
---
## Examples
- `src/features/SkillStore/index.tsx`
- `src/features/LibraryModal/CreateNew/index.tsx`
- Base-ui (preferred): follow sections above; ensure **base-ui `ModalHost`** is mounted.
- Legacy: `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`
+4 -1
View File
@@ -6,6 +6,9 @@ description: React component development guide. Use when working with React comp
# React Component Writing Guide
- Use antd-style for complex styles; for simple cases, use inline `style` attribute
- **Prefer `createStaticStyles` with `cssVar.*`** (zero-runtime) — module-level, no hook call required
- Only fall back to `createStyles` + `token` when styles genuinely need runtime computation (dynamic props, JS color fns like `readableColor`/`chroma`)
- See `.cursor/docs/createStaticStyles_migration_guide.md` for full pattern
- Use `Flexbox` and `Center` from `@lobehub/ui` for layouts (see `references/layout-kit.md`)
- Component priority: `src/components` > `@lobehub/ui/base-ui` > `@lobehub/ui` > custom implementation
- Always prefer `@lobehub/ui/base-ui` primitives (Select, Modal, DropdownMenu, Popover, Switch, ScrollArea…) over antd equivalents
@@ -64,7 +67,7 @@ import { dynamicElement, redirectElement, ErrorBoundary } from '@/utils/router';
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
element: redirectElement('/settings/profile');
errorElement: <ErrorBoundary resetPath="/chat" />;
errorElement: <ErrorBoundary />;
```
### Navigation
-114
View File
@@ -1,114 +0,0 @@
---
name: recent-data
description: Guide for using Recent Data (topics, resources, pages). Use when working with recently accessed items, implementing recent lists, or accessing session store recent data. Triggers on recent data usage or implementation tasks.
user-invocable: false
---
# Recent Data Usage Guide
Recent data (recentTopics, recentResources, recentPages) is stored in session store.
## Initialization
In app top-level (e.g., `RecentHydration.tsx`):
```tsx
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
const App = () => {
useInitRecentTopic();
useInitRecentResource();
useInitRecentPage();
return <YourComponents />;
};
```
## Usage
### Method 1: Read from Store (Recommended)
```tsx
import { useSessionStore } from '@/store/session';
import { recentSelectors } from '@/store/session/selectors';
const Component = () => {
const recentTopics = useSessionStore(recentSelectors.recentTopics);
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
if (!isInit) return <div>Loading...</div>;
return (
<div>
{recentTopics.map((topic) => (
<div key={topic.id}>{topic.title}</div>
))}
</div>
);
};
```
### Method 2: Use Hook Return (Single component)
```tsx
const { data: recentTopics, isLoading } = useInitRecentTopic();
```
## Available Selectors
### Recent Topics
```tsx
const recentTopics = useSessionStore(recentSelectors.recentTopics);
// Type: RecentTopic[]
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
// Type: boolean
```
**RecentTopic type:**
```typescript
interface RecentTopic {
agent: {
avatar: string | null;
backgroundColor: string | null;
id: string;
title: string | null;
} | null;
id: string;
title: string | null;
updatedAt: Date;
}
```
### Recent Resources
```tsx
const recentResources = useSessionStore(recentSelectors.recentResources);
// Type: FileListItem[]
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
```
### Recent Pages
```tsx
const recentPages = useSessionStore(recentSelectors.recentPages);
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
```
## Features
1. **Auto login detection**: Only loads when user is logged in
2. **Data caching**: Stored in store, no repeated loading
3. **Auto refresh**: SWR refreshes on focus (5-minute interval)
4. **Type safe**: Full TypeScript types
## Best Practices
1. Initialize all recent data at app top-level
2. Use selectors to read from store
3. For multi-component use, prefer Method 1
4. Use selectors for render optimization
+203 -44
View File
@@ -1,10 +1,27 @@
---
name: version-release
description: "Version release workflow. Use when the user mentions 'release', 'hotfix', 'version upgrade', 'weekly release', or '发版'/'发布'/'小班车'. Provides guides for Minor Release and Patch Release workflows."
description: "Version release workflow. Use when the user mentions 'release', 'hotfix', 'version upgrade', 'weekly release', or '发版'/'发布'/'小班车'. This skill is for release process and GitHub Release notes (not docs/changelog page writing)."
---
# Version Release Workflow
## Scope Boundary (Important)
This skill is only for:
1. Release branch / PR workflow
2. CI trigger constraints (`auto-tag-release.yml`)
3. GitHub Release note writing
This skill is **not** for writing `docs/changelog/*.mdx`.\
If the user asks for website changelog pages, load `../docs-changelog/SKILL.md`.
## Mandatory Companion Skill
For every `/version-release` execution, you MUST load and apply:
- `../microcopy/SKILL.md`
## Overview
The primary development branch is **canary**. All day-to-day development happens on canary. When releasing, canary is merged into main. After merge, `auto-tag-release.yml` automatically handles tagging, version bumping, creating a GitHub Release, and syncing back to the canary branch.
@@ -18,7 +35,7 @@ Only two release types are used in practice (major releases are extremely rare a
## Minor Release Workflow
Used to publish a new minor version (e.g. v2.2.0), roughly every 4 weeks.
Used to publish a new minor version (e.g. `v2.2.0`), roughly every 4 weeks.
### Steps
@@ -31,7 +48,7 @@ git checkout -b release/v{version}
git push -u origin release/v{version}
```
2. **Determine the version number** — Read the current version from `package.json` and compute the next minor version (e.g. 2.1.x 2.2.0)
2. **Determine the version number** — Read the current version from `package.json` and compute the next minor version (e.g. 2.1.x -> 2.2.0)
3. **Create a PR to main**
@@ -43,9 +60,10 @@ gh pr create \
--body "## 📦 Release v{version} ..."
```
> \[!IMPORTANT]: The PR title must strictly match the `🚀 release: v{x.y.z}` format. CI uses a regex on this title to determine the exact version number.
> \[!IMPORTANT]
> The PR title must strictly match the `🚀 release: v{x.y.z}` format. CI uses a regex on this title to determine the exact version number.
4. **Automatic trigger after merge**: auto-tag-release detects the title format and uses the version number from the title to complete the release.
4. **Automatic trigger after merge**: `auto-tag-release` detects the title format and uses the version number from the title to complete the release.
### Scripts
@@ -60,7 +78,7 @@ Version number is automatically bumped by patch +1. There are 4 common scenarios
| Scenario | Source Branch | Branch Naming | Description |
| ------------------- | ------------- | ----------------------------- | ------------------------------------------------ |
| Weekly Release | canary | `release/weekly-{YYYYMMDD}` | Weekly release train, canary main |
| Weekly Release | canary | `release/weekly-{YYYYMMDD}` | Weekly release train, canary -> main |
| Bug Hotfix | main | `hotfix/v{version}-{hash}` | Emergency bug fix |
| New Model Launch | canary | Community PR merged directly | New model launch, triggered by PR title prefix |
| DB Schema Migration | main | `release/db-migration-{name}` | Database migration, requires dedicated changelog |
@@ -73,19 +91,19 @@ All scenarios auto-bump patch +1. Patch PR titles do not need a version number.
bun run hotfix:branch # Hotfix scenario
```
## Auto-Release Trigger Rules (auto-tag-release.yml)
## Auto-Release Trigger Rules (`auto-tag-release.yml`)
After a PR is merged into main, CI determines whether to release based on the following priority:
### 1. Minor Release (Exact Version)
PR title matches `🚀 release: v{x.y.z}` uses the version number from the title.
PR title matches `🚀 release: v{x.y.z}` -> uses the version number from the title.
### 2. Patch Release (Auto patch +1)
Triggered by the following priority:
- **Branch name match**: `hotfix/*` or `release/*` triggers directly (skips title detection)
- **Branch name match**: `hotfix/*` or `release/*` -> triggers directly (skips title detection)
- **Title prefix match**: PRs with the following title prefixes will trigger:
- `style` / `💄 style`
- `feat` / `✨ feat`
@@ -96,64 +114,205 @@ Triggered by the following priority:
### 3. No Trigger
PRs that don't match any of the above conditions (e.g. `docs`, `chore`, `ci`, `test` prefixes) will not trigger a release when merged into main.
PRs that don't match any conditions above (e.g. `docs`, `chore`, `ci`, `test`) will not trigger a release when merged into main.
## Post-Release Automated Actions
1. **Bump package.json** — commits `🔖 chore(release): release version v{x.y.z} [skip ci]`
1. **Bump `package.json`** — commits `🔖 chore(release): release version v{x.y.z} [skip ci]`
2. **Create annotated tag**`v{x.y.z}`
3. **Create GitHub Release**
4. **Dispatch sync-main-to-canary** — syncs main back to the canary branch
4. **Dispatch `sync-main-to-canary`** — syncs main back to canary
## Claude Action Guide
## Agent Action Guide
When the user requests a release:
### Minor Release
1. Read `package.json` to get the current version and compute the next minor version
2. Create a `release/v{version}` branch from canary
3. Push and create a PR — **title must be `🚀 release: v{version}`**
4. Inform the user that merging the PR will automatically trigger the release
### Precheck
Before creating the release branch, verify the source branch:
- **Weekly Release** (`release/weekly-*`): must branch from `canary`
- **All other release/hotfix branches**: must branch from `main` run `git merge-base --is-ancestor main <branch> && echo OK` to confirm
- If the branch is based on the wrong source, delete and recreate from the correct base
- **All other release/hotfix branches**: must branch from `main`; run `git merge-base --is-ancestor main <branch> && echo OK`
- If the branch is based on the wrong source, recreate from the correct base
### Minor Release
1. Read `package.json` to get the current version and compute the next minor version
2. Create a `release/v{version}` branch from canary
3. Push and create PR — **title must be `🚀 release: v{version}`**
4. Inform the user that merge will auto-trigger release
### Patch Release
Choose the appropriate workflow based on the scenario (see `reference/patch-release-scenarios.md`):
Choose workflow by scenario (see `reference/patch-release-scenarios.md`):
- **Weekly Release**: Create a `release/weekly-{YYYYMMDD}` branch from canary, scan `git log main..canary` to write the changelog, title like `🚀 release: 20260222`
- **Bug Hotfix**: Create a `hotfix/` branch from main, use a gitmoji prefix title (e.g. `🐛 fix: ...`)
- **New Model Launch**: Community PRs trigger automatically via title prefix (`feat` / `style`), no extra steps needed
- **DB Migration**: Create a `release/db-migration-{name}` branch from main, cherry-pick migration commits, write a dedicated migration changelog
- **Weekly Release**: create `release/weekly-{YYYYMMDD}` from canary; use `git log main..canary` for release note inputs; title like `🚀 release: 20260222`
- **Bug Hotfix**: create `hotfix/` from main; use gitmoji prefix title (e.g. `🐛 fix: ...`)
- **New Model Launch**: community PRs trigger automatically via title prefix (`feat` / `style`)
- **DB Migration**: create `release/db-migration-{name}` from main; cherry-pick migration commits; include dedicated migration notes
### Important Notes
### Hard Rules
- **Do NOT manually modify the version in package.json** — CI will auto-bump it
- **Do NOT manually create tags** — CI will create them automatically
- The Minor Release PR title format is a hard requirement — incorrect format will not use the specified version number
- Patch PRs do not need a version number — CI auto-bumps patch +1
- All release PRs must include a user-facing changelog
- **Do NOT** manually modify `package.json` version
- **Do NOT** manually create tags
- Minor PR title format is strict
- Patch PRs do not need explicit version number
- Keep release facts accurate; do not invent metrics or availability statements
## Changelog Writing Guidelines
## GitHub Release Changelog Standard (Long-Form Style)
All release PR bodies (both Minor and Patch) must include a user-facing changelog. Scan changes via `git log main..canary --oneline` or `git diff main...canary --stat`, then write following the format below.
Use this section for writing **GitHub Release notes** (or release PR body when the PR body is intended to become release notes).\
Do not use this as `docs/changelog` page guidance.
### Format Reference
### Positioning
- Weekly Release: See `reference/changelog-example/weekly-release.md`
- DB Migration: See `reference/changelog-example/db-migration.md`
This release-note style is:
### Writing Tips
1. **Data-backed at the top** (date, range, key metrics)
2. **Narrative first, then structured detail**
3. **Deep but scannable** (clear sectioning + compact bullets)
4. **Contributor-forward** (credits are part of the release story)
- **User-facing**: Describe changes that users can perceive, not internal implementation details
- **Clear categories**: Group by features, models/providers, desktop, stability/fixes, etc.
- **Highlight key items**: Use `**bold**` for important feature names
- **Credit contributors**: Collect all committers via `git log` and list alphabetically
- **Flexible categories**: Choose categories based on actual changes — no need to force-fit all categories
### Required Inputs Before Writing
Collect these inputs first:
1. Compare range (`<prev_tag>...<current_tag>`)
2. Release metrics (commits, merged PRs, resolved issues, contributors, optional files/insertions/deletions)
3. High-impact changes by domain (core loop, platform/gateway, UX, tooling, security, reliability)
4. Contributor list (with standout contributions if known)
5. Known risks / migrations / rollout notes (if any)
If metrics cannot be reliably computed, omit unknown numbers instead of guessing.
### Canonical Structure
Follow this section order unless the user asks otherwise:
1. `# 🚀 LobeHub v<x.y.z> (<YYYYMMDD>)`
2. Metadata lines:
- `Release Date`
- `Since <Previous Version>` metrics
3. One quoted release thesis (single paragraph, 1-2 lines)
4. `## ✨ Highlights` (6-12 bullets for major releases; 3-8 for weekly)
5. Domain blocks with optional `###` subsections:
- `## 🏗️ Core Agent & Architecture` (or equivalent product core)
- `## 📱 Platforms / Integrations`
- `## 🖥️ CLI & User Experience`
- `## 🔧 Tooling`
- `## 🔒 Security & Reliability`
- `## 📚 Documentation` (optional if meaningful)
6. `## 👥 Contributors`
7. `**Full Changelog**: <prev>...<current>`
Use `---` separators between major blocks for long releases.
### Writing Rules (Hard)
1. **No fabricated metrics**: all numbers must be traceable.
2. **No vague headline bullets**: each bullet must include capability + impact.
3. **No internal-only framing**: phrase from user/operator perspective.
4. **Security must be explicit** when security-sensitive fixes are present.
5. **PR/issue linkage**: use `(#1234)` when IDs are available.
6. **Terminology consistency**: same feature/provider name across sections.
7. **Do not bury migration or breaking changes**: elevate to dedicated section or callout.
### Style Rules (Long-Form)
1. Start with an "everyday use" framing, not implementation internals.
2. Mix narrative sentence + evidence bullets.
3. Keep bullets compact but informative:
- Good: `**Fast Mode (`/fast`)** — Priority routing for OpenAI and Anthropic, reducing latency on supported models. (#6875, #6960)`
4. Use bold only for capability names, not for whole sentences.
5. Keep heading depth <= 3 levels.
### Release Size Heuristics
- **Minor / major milestone release**
- Include full structure with multiple domain blocks.
- `Highlights` usually 8-12 bullets.
- **Weekly patch release**
- Keep full skeleton but reduce subsection count.
- `Highlights` usually 4-8 bullets.
- **DB migration release**
- Keep concise.
- Must include `Migration overview`, operator impact, and rollback/backup note.
### GitHub Release Changelog Template
```md
# 🚀 LobeHub v<x.y.z> (<YYYYMMDD>)
**Release Date:** <Month DD, YYYY>
**Since <Previous Version>:** <N commits> · <N merged PRs> · <N resolved issues> · <N contributors>
> <One release thesis sentence: what this release unlocks in practice.>
---
## ✨ Highlights
- **<Capability A>** — <What changed and why it matters>. (#1234)
- **<Capability B>** — <What changed and why it matters>. (#2345)
- **<Capability C>** — <What changed and why it matters>. (#3456)
---
## 🏗️ Core Product & Architecture
### <Subdomain>
- <Concrete change + impact>. (#...)
- <Concrete change + impact>. (#...)
---
## 📱 Platforms / Integrations
- <Platform update + impact>. (#...)
- <Compatibility/reliability fix + impact>. (#...)
---
## 🖥️ CLI & User Experience
- <User-facing workflow improvement>. (#...)
- <Quality-of-life fix>. (#...)
---
## 🔧 Tooling
- <Tool/runtime improvement>. (#...)
---
## 🔒 Security & Reliability
- **Security:** <hardening or vulnerability fix>. (#...)
- **Reliability:** <stability/performance behavior improvement>. (#...)
---
## 👥 Contributors
**<N merged PRs>** from **<N contributors>** across **<N commits>**.
### Community Contributors
- @<username> - <notable contribution area>
- @<username> - <notable contribution area>
---
**Full Changelog**: <previous_tag>...<current_tag>
```
### Quick Checklist
- [ ] Uses top metadata and a clear release thesis
- [ ] Includes `Highlights` plus domain-grouped sections
- [ ] Every major bullet states both change and user/operator impact
- [ ] Security and reliability updates are explicitly surfaced (when present)
- [ ] Contributor credits and compare range are included
- [ ] All numbers and claims are verifiable
@@ -1,20 +1,60 @@
# DB Schema Migration Changelog Example
# 🚀 LobeHub v2.1.50 (20260416)
A changelog reference for database migration release PR bodies.
**Release Date:** April 20, 2026\
**Migration Scope:** Agent benchmark data model bootstrap (5 new tables, 2 new indexes)
> This release introduces a schema foundation for benchmark execution and reporting, so agent evaluation data is stored as a complete lifecycle instead of fragmented records.
---
This release includes a **database schema migration** involving **5 new tables** for the Agent Evaluation Benchmark system.
## ✨ Highlights
### Migration: Add Agent Evaluation Benchmark Tables
- **Benchmark Lifecycle Schema** — Added a relational model that tracks benchmark setup, runs, per-topic execution, and record outputs end-to-end.
- **Queryability Upgrade** — Added indexes for run status and benchmark-topic joins, improving operational queries in dashboard and debugging workflows.
- **Safer Operator Rollout** — Migration is startup-driven and backward-compatible with existing non-benchmark chat workflows.
- Added 5 new tables: `agent_eval_benchmarks`, `agent_eval_datasets`, `agent_eval_records`, `agent_eval_runs`, `agent_eval_run_topics`
---
### Notes for Self-hosted Users
## 🗄️ Migration Overview
- The migration runs automatically on application startup
- No manual intervention required
Added tables:
The migration owner: @{pr-author} — responsible for this database schema change, reach out for any migration-related issues.
- `agent_eval_benchmarks`
- `agent_eval_datasets`
- `agent_eval_runs`
- `agent_eval_run_topics`
- `agent_eval_records`
> **Note for Claude**: Replace `{pr-author}` with the actual PR author. Retrieve via `gh pr view <number> --json author --jq '.author.login'` or `git log` commit author. Do NOT hardcode a username.
Added indexes:
- `idx_agent_eval_runs_status_created_at`
- `idx_agent_eval_run_topics_run_id_topic_id`
These additions close a previous gap where benchmark data existed in partial forms but lacked a stable relational backbone for auditing and historical analysis.
---
## ⚙️ Operator Notes
- Migration runs automatically on application startup.
- No manual SQL is required in standard deployment paths.
- Schedule rollout in a low-traffic window and take a backup snapshot before deployment.
- If migration fails, do not retry repeatedly; inspect migration logs and lock state first.
---
## 🔒 Reliability & Risk
- Existing chat/session paths are unaffected unless benchmark features are enabled.
- Migration is additive (new tables/indexes only), minimizing downgrade risk to existing entities.
- Rollback should follow your standard DB restore or migration rollback policy if your environment requires strict reversibility.
---
## 👥 Owner
Migration owner: @{pr-author}
The migration owner is responsible for rollout follow-up and incident handling for this schema change.
> **Note for Claude**: Replace `{pr-author}` with the actual PR author. Retrieve via `gh pr view <number> --json author --jq '.author.login'` or from commit metadata. Do not hardcode a username.
@@ -1,46 +1,80 @@
# Patch Release (Weekly) Changelog Example
# 🚀 LobeHub v2.1.50 (20260420)
A real-world changelog reference for weekly patch release PR bodies.
**Release Date:** April 20, 2026\
**Since v2026.04.13:** 96 commits · 58 merged PRs · 31 resolved issues · 17 contributors
> This weekly release focuses on reducing friction in everyday agent work: faster model routing, smoother gateway behavior, stronger task continuity, and clearer operator diagnostics when something goes wrong.
---
This release includes **82 commits** , Key updates are below.
## ✨ Highlights
### New Features and Enhancements
- **Gateway Session Recovery** — Agent sessions now recover more reliably after short network interruptions, so long-running tasks continue with less manual retry. (#10121, #10133)
- **Fast Model Routing** — Expanded low-latency routing for priority model tiers, reducing wait time in high-frequency generation workflows. (#10102, #10117)
- **Agent Task Workspace** — Running tasks now remain isolated from main chat state, which keeps primary conversations cleaner while background work progresses. (#10088)
- **Provider Coverage Update** — Added support for new model variants across OpenAI-compatible and regional providers, improving fallback options in production. (#10094, #10109)
- **Desktop Attachment Flow** — File and screenshot attachment behavior is more predictable in desktop sessions, especially for mixed text + media prompts. (#10073)
- **Security Hardening Pass** — Closed multiple input validation gaps in webhook and file-path handling paths. (#10141, #10152)
- Added **Agent Benchmark** support for more systematic agent performance evaluation.
- Introduced the **video generation** feature end-to-end, including entry points, sidebar "new" badge support, and skeleton loading for topic switching.
- Expanded memory capabilities: support for memory effort/tool permission configuration and improved timeout calculation for memory analysis tasks.
- Added desktop editor support for image upload via file picker.
---
### Models and Provider Expansion
## 🏗️ Core Agent & Architecture
- Added a new provider: **Straico**.
- Added/updated support for:
- Claude Sonnet 4.6
- Gemini 3.1 Pro Preview
- Qwen3.5 series
- Grok Imagine (`grok-imagine-image`)
- MiniMax 2.5
- Added related i18n copy and model parameter adaptations.
### Agent loop and context handling
### Desktop Improvements
- Improved context compaction thresholds to reduce mid-task exits under tight token budgets. (#10079)
- Added better diagnostics for tool-call truncation and recovery behavior during streamed responses. (#10106)
- Refined delegate task activity propagation to improve parent-child task status consistency. (#10098)
- Integrated `electron-liquid-glass` (macOS Tahoe).
- Improved DMG background assets and desktop release workflow.
### Provider and model behavior
### Stability, Security, and UX Fixes
- Unified provider-side timeout handling in fallback chains to reduce false failure classification. (#10097)
- Updated reasoning-model defaults and response normalization for better cross-provider consistency. (#10109)
- Fixed multiple video generation pipeline issues: precharge refund handling, webhook token verification, pricing parameter usage, asset cleanup, and type safety.
- Fixed `sanitizeFileName` path traversal risks and added unit tests.
- Fixed MCP media URL generation with duplicated `APP_URL` prefix.
- Fixed Qwen3 embedding failures caused by batch-size limits.
- Fixed multiple UI/interaction issues, including mobile header agent selector/topic count, ChatInput scrolling behavior, and tooltip stacking context.
- Fixed missing `@napi-rs/canvas` native bindings in Docker standalone builds.
- Improved GitHub Copilot authentication retry behavior and response error handling in edge cases.
---
### Credits
## 📱 Gateway & Platform Integrations
Huge thanks to these contributors (alphabetical):
- Gateway now drains in-flight events more safely before restart, reducing duplicate notification bursts. (#10125)
- Discord and Slack adapters received retry/backoff tuning for unstable webhook windows. (#10091, #10119)
- WeCom callback-mode message state persistence now uses safer atomic updates. (#10114)
@AmAzing129 @Coooolfan @Innei @ONLY-yours @Zhouguanyang @arvinxx @eaten-cake @hezhijie0327 @nekomeowww @rdmclin2 @rivertwilight @sxjeru @tjx666
---
## 🖥️ CLI & User Experience
- Improved slash command discoverability in CLI and gateway contexts with clearer hint messages. (#10086)
- `/model` switching feedback now returns clearer success/failure states in cross-platform chats. (#10108)
- Setup flow now warns earlier about missing provider credentials in first-run scenarios. (#10115)
---
## 🔧 Tooling
- MCP registration flow now validates duplicate tool names before activation, reducing runtime conflicts. (#10093)
- Browser tooling improved stale-session cleanup to prevent orphaned local resources. (#10112)
---
## 🔒 Security & Reliability
- **Security:** Hardened path sanitization for uploaded assets and webhook callback validation. (#10141, #10152)
- **Reliability:** Reduced empty-response retry storms by refining retry-classification conditions. (#10130)
- **Reliability:** Improved timeout defaults for long-running background processes in constrained environments. (#10122)
---
## 👥 Contributors
**58 merged PRs** from **17 contributors** across **96 commits**.
### Community Contributors
- @alice-example - Gateway recovery and retry improvements
- @bob-example - Provider fallback normalization
- @charlie-example - Desktop media attachment flow
- @dora-example - Webhook validation hardening
---
**Full Changelog**: v2026.04.13...v2026.04.20
+5
View File
@@ -136,6 +136,11 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# MOONSHOT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ## Kimi Code Plan ####
# KIMICODINGPLAN_PROXY_URL=https://api.kimi.com/coding
# KIMICODINGPLAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ## Minimax AI ####
# MINIMAX_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+61 -21
View File
@@ -1,7 +1,7 @@
name: Release ModelBank
permissions:
contents: write
contents: read
id-token: write
on:
@@ -41,15 +41,12 @@ jobs:
publish:
name: Publish ModelBank
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -63,27 +60,70 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Bump patch version
id: version
run: |
npm version patch --no-git-tag-version --prefix packages/model-bank
echo "version=$(node -p 'require(\"./packages/model-bank/package.json\").version')" >> "$GITHUB_OUTPUT"
- name: Build package
run: pnpm --filter model-bank build
- name: Prepare publish package
id: version
run: |
BASE_VERSION=$(node -p "require('./packages/model-bank/package.json').version.split('.').slice(0, 2).join('.')")
MODEL_BANK_VERSION="${BASE_VERSION}.$(date -u +%Y%m%d%H%M%S)"
export MODEL_BANK_VERSION
node <<'NODE'
const fs = require('node:fs');
const packagePath = 'packages/model-bank/package.json';
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const toDistExport = (sourcePath) => sourcePath.replace('./src/', './dist/').replace(/\.ts$/, '.mjs');
packageJson.version = process.env.MODEL_BANK_VERSION;
packageJson.type = 'module';
packageJson.main = './dist/index.mjs';
packageJson.types = './dist/index.d.mts';
packageJson.files = ['dist'];
packageJson.repository = {
type: 'git',
url: 'https://github.com/lobehub/lobehub',
directory: 'packages/model-bank',
};
packageJson.exports = Object.fromEntries(
Object.entries(packageJson.exports).map(([key, value]) => {
if (typeof value !== 'string') return [key, value];
const distPath = toDistExport(value);
return [
key,
{
types: distPath.replace(/\.mjs$/, '.d.mts'),
import: distPath,
default: distPath,
},
];
}),
);
delete packageJson.private;
delete packageJson.devDependencies;
delete packageJson.scripts;
if (packageJson.dependencies) {
delete packageJson.dependencies['@lobechat/business-const'];
if (Object.keys(packageJson.dependencies).length === 0) {
delete packageJson.dependencies;
}
}
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
NODE
echo "version=${MODEL_BANK_VERSION}" >> "$GITHUB_OUTPUT"
echo "Prepared model-bank@${MODEL_BANK_VERSION}"
- name: Publish to npm
run: npm publish --provenance
run: npm publish --provenance --access public
working-directory: packages/model-bank
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Commit version bump
env:
MODEL_BANK_VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name "lobehubbot"
git config user.email "i@lobehub.com"
git add packages/model-bank/package.json
git commit -m "🔖 chore(model-bank): release v${MODEL_BANK_VERSION}"
git push
+3 -3
View File
@@ -97,8 +97,8 @@ jobs:
if: needs.check-duplicate-run.outputs.should_skip != 'true'
strategy:
matrix:
shard: [1, 2]
name: Test App (shard ${{ matrix.shard }}/2)
shard: [1, 2, 3]
name: Test App (shard ${{ matrix.shard }}/3)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -110,7 +110,7 @@ jobs:
run: pnpm install
- name: Run tests
run: bunx vitest --coverage --silent='passed-only' --reporter=default --reporter=blob --shard=${{ matrix.shard }}/2
run: bunx vitest --coverage --silent='passed-only' --reporter=default --reporter=blob --shard=${{ matrix.shard }}/3
- name: Upload blob report
if: ${{ !cancelled() }}
+1 -1
View File
@@ -146,4 +146,4 @@ apps/desktop/resources/cli-package.json
# Superpowers plugin brainstorm/spec outputs (local only; do not commit)
.superpowers/
docs/superpowers/
.heerogeneous-tracing
+6 -4
View File
@@ -6,7 +6,11 @@
},
"editor.formatOnSave": true,
// don't show errors, but fix when save and git pre commit
"eslint.rules.customizations": [],
"eslint.rules.customizations": [
{ "rule": "simple-import-sort/exports", "severity": "off" },
{ "rule": "perfectionist/sort-interfaces", "severity": "off" },
{ "rule": "simple-import-sort/imports", "severity": "off" }
],
"eslint.validate": [
"json",
"javascript",
@@ -16,7 +20,7 @@
// support mdx
"mdx"
],
"mdx.server.enable": false,
"js/ts.tsdk.path": "node_modules/typescript/lib",
"npm.packageManager": "pnpm",
"search.exclude": {
"**/node_modules": true,
@@ -44,9 +48,7 @@
// make stylelint work with tsx antd-style css template string
"typescriptreact"
],
"typescript.tsdk": "node_modules/typescript/lib",
"vitest.disableWorkspaceWarning": true,
"vitest.maximumConfigs": 10,
"workbench.editor.customLabels.patterns": {
"**/app/**/[[]*[]]/[[]*[]]/page.tsx": "${dirname(2)}/${dirname(1)}/${dirname} • page component",
"**/app/**/[[]*[]]/page.tsx": "${dirname(1)}/${dirname} • page component",
+94
View File
@@ -2,6 +2,100 @@
# Changelog
## [Version 2.1.51](https://github.com/lobehub/lobe-chat/compare/v0.0.0-nightly.pr13850.8503...v2.1.51)
<sup>Released on **2026-04-16**</sup>
#### 👷 Build System
- **database**: add document history schema.
- **database**: add document history schema.
#### 🐛 Bug Fixes
- **misc**: fix minify cli.
- **misc**: recent delete.
- **deps**: pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg.
- **database**: enforce document history ownership and pagination.
#### ✨ Features
- **database**: add document history table and update related models.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Build System
- **database**: add document history schema, closes [#13789](https://github.com/lobehub/lobe-chat/issues/13789) ([c1174d3](https://github.com/lobehub/lobe-chat/commit/c1174d3))
- **database**: add document history schema ([e3eef04](https://github.com/lobehub/lobe-chat/commit/e3eef04))
#### What's fixed
- **misc**: fix minify cli, closes [#13888](https://github.com/lobehub/lobe-chat/issues/13888) ([cb4ad01](https://github.com/lobehub/lobe-chat/commit/cb4ad01))
- **misc**: recent delete, closes [#13878](https://github.com/lobehub/lobe-chat/issues/13878) ([85227cf](https://github.com/lobehub/lobe-chat/commit/85227cf))
- **deps**: pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg ([d526b40](https://github.com/lobehub/lobe-chat/commit/d526b40))
- **database**: enforce document history ownership and pagination ([b9c4b87](https://github.com/lobehub/lobe-chat/commit/b9c4b87))
#### What's improved
- **database**: add document history table and update related models ([64fc6d4](https://github.com/lobehub/lobe-chat/commit/64fc6d4))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.1.50](https://github.com/lobehub/lobe-chat/compare/v2.1.49...v2.1.50)
<sup>Released on **2026-04-16**</sup>
#### 👷 Build System
- **database**: add document history schema.
- **database**: add document history schema.
#### 🐛 Bug Fixes
- **deps**: pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg.
- **database**: enforce document history ownership and pagination.
#### ✨ Features
- **database**: add document history table and update related models.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Build System
- **database**: add document history schema, closes [#13789](https://github.com/lobehub/lobe-chat/issues/13789) ([c1174d3](https://github.com/lobehub/lobe-chat/commit/c1174d3))
- **database**: add document history schema ([e3eef04](https://github.com/lobehub/lobe-chat/commit/e3eef04))
#### What's fixed
- **deps**: pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg ([d526b40](https://github.com/lobehub/lobe-chat/commit/d526b40))
- **database**: enforce document history ownership and pagination ([b9c4b87](https://github.com/lobehub/lobe-chat/commit/b9c4b87))
#### What's improved
- **database**: add document history table and update related models ([64fc6d4](https://github.com/lobehub/lobe-chat/commit/64fc6d4))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 2.1.45](https://github.com/lobehub/lobe-chat/compare/v2.1.44...v2.1.45)
<sup>Released on **2026-03-26**</sup>
+1 -1
View File
@@ -6,7 +6,7 @@ Guidelines for using Claude Code in this LobeHub repository.
- Next.js 16 + React 19 + TypeScript
- SPA inside Next.js with `react-router-dom`
- `@lobehub/ui`, antd for components; antd-style for CSS-in-JS
- `@lobehub/ui`, antd for components; antd-style for CSS-in-JS**prefer `createStaticStyles` with `cssVar.*`** (zero-runtime); only fall back to `createStyles` + `token` when styles genuinely need runtime computation. See `.cursor/docs/createStaticStyles_migration_guide.md`.
- react-i18next for i18n; zustand for state management
- SWR for data fetching; TRPC for type-safe backend
- Drizzle ORM with PostgreSQL; Vitest for testing
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Code generated by `npm run man:generate`; DO NOT EDIT.
.\" Manual command details come from the Commander command tree.
.TH LH 1 "" "@lobehub/cli 0.0.6" "User Commands"
.TH LH 1 "" "@lobehub/cli 0.0.8" "User Commands"
.SH NAME
lh \- LobeHub CLI \- manage and connect to LobeHub services
.SH SYNOPSIS
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/cli",
"version": "0.0.6",
"version": "0.0.8",
"type": "module",
"bin": {
"lh": "./dist/index.js",
@@ -28,6 +28,7 @@
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@lobechat/agent-gateway-client": "workspace:*",
"@lobechat/device-gateway-client": "workspace:*",
"@lobechat/local-file-shell": "workspace:*",
"@trpc/client": "^11.8.1",
+1
View File
@@ -1,4 +1,5 @@
packages:
- '../../packages/agent-gateway-client'
- '../../packages/device-gateway-client'
- '../../packages/local-file-shell'
- '../../packages/file-loaders'
+6 -1
View File
@@ -466,7 +466,12 @@ export function registerTaskCommand(program: Command) {
: act.priority === 'normal'
? pc.yellow(' [normal]')
: '';
const resolved = act.resolvedAction ? pc.green(` ✏️ ${act.resolvedAction}`) : '';
const resolvedLabel = act.resolvedAction
? act.resolvedComment
? `${act.resolvedAction}: ${act.resolvedComment}`
: act.resolvedAction
: '';
const resolved = resolvedLabel ? pc.green(` ✏️ ${resolvedLabel}`) : '';
const typeLabel = pc.dim(`[${act.briefType}]`);
console.log(
` ${icon} ${pc.dim(ago.padStart(7))} Brief ${typeLabel} ${act.title}${pri}${resolved}${idSuffix}`,
+2 -8
View File
@@ -1,16 +1,10 @@
import type { AgentStreamEvent } from '@lobechat/agent-gateway-client';
import pc from 'picocolors';
import urlJoin from 'url-join';
import { log } from './logger';
export interface AgentStreamEvent {
data: any;
id?: string;
operationId: string;
stepIndex: number;
timestamp: number;
type: string;
}
export type { AgentStreamEvent } from '@lobechat/agent-gateway-client';
interface StreamOptions {
json?: boolean;
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
entry: ['src/index.ts'],
fixedExtension: false,
format: ['esm'],
minify: true,
minify: !!process.env.MINIFY,
outputOptions: {
codeSplitting: false,
},
+1 -1
View File
@@ -112,7 +112,7 @@ const config = {
// Build and copy CLI bundle for embedding
console.info('📦 Building CLI for embedding...');
execSync('npm run build', { stdio: 'inherit', cwd: path.resolve(__dirname, '../cli') });
execSync('npm run build:cli', { stdio: 'inherit', cwd: __dirname });
const cliSrc = path.resolve(__dirname, '../cli/dist/index.js');
const cliDest = path.resolve(__dirname, 'resources/bin/lobe-cli.js');
await fs.copyFile(cliSrc, cliDest);
+57 -4
View File
@@ -15,15 +15,64 @@ import {
import { getExternalDependencies } from './native-deps.config.mjs';
/**
* Rewrite `/` to `/apps/desktop/index.html` so the electron-vite dev server
* serves the desktop HTML entry when root is the monorepo root.
* Force `base: '/'` in renderer config. The `electron-vite` preset
* unconditionally rewrites base to `'./'` in production (with `enforce: 'pre'`),
* which produces relative asset URLs like `../../assets/...`. Those break in
* the popup window because its SPA URL (`/popup/agent/:aid/:tid`) is deep
* enough that relative resolution lands at `/popup/assets/...` instead of the
* actual `/assets/...`. Our `app://` protocol handler resolves absolute
* `/assets/...` correctly regardless of URL depth.
*/
function forceAbsoluteBasePlugin(): PluginOption {
return {
name: 'electron-desktop-force-base',
config(config) {
config.base = '/';
},
};
}
/**
* Rewrite SPA routes to their corresponding HTML entry so the electron-vite
* dev server serves the right HTML when root is the monorepo root.
*
* - `/popup/*` → `/apps/desktop/popup.html` (topic popup SPA)
* - `/`, `/index.html`, and everything else → `/apps/desktop/index.html`
*/
function electronDesktopHtmlPlugin(): PluginOption {
return {
configureServer(server: ViteDevServer) {
server.middlewares.use((req, _res, next) => {
if (req.url === '/' || req.url === '/index.html') {
const rawUrl = req.url ?? '';
const pathname = rawUrl.split('?')[0];
// Explicit document-entry requests — always rewrite.
if (pathname === '/' || pathname === '/index.html') {
req.url = '/apps/desktop/index.html';
next();
return;
}
if (pathname === '/popup.html') {
req.url = '/apps/desktop/popup.html';
next();
return;
}
// For SPA deep links (e.g. `/popup/agent/A/T`) rewrite to the popup
// HTML — but skip asset / module requests that happen to share the
// prefix (e.g. `/popup/@vite/client` would have been generated by a
// mis-resolved relative import).
const lastSegment = pathname.split('/').pop() ?? '';
const looksLikeAsset =
lastSegment.includes('.') ||
pathname.startsWith('/@') ||
pathname.startsWith('/src/') ||
pathname.startsWith('/node_modules/') ||
pathname.startsWith('/apps/') ||
pathname.startsWith('/packages/');
if (!looksLikeAsset && (pathname === '/popup' || pathname.startsWith('/popup/'))) {
req.url = '/apps/desktop/popup.html';
}
next();
});
@@ -102,7 +151,10 @@ export default defineConfig({
build: {
outDir: path.resolve(__dirname, 'dist/renderer'),
rollupOptions: {
input: path.resolve(__dirname, 'index.html'),
input: {
main: path.resolve(__dirname, 'index.html'),
popup: path.resolve(__dirname, 'popup.html'),
},
output: sharedRollupOutput,
},
},
@@ -112,6 +164,7 @@ export default defineConfig({
},
optimizeDeps: sharedOptimizeDeps,
plugins: [
forceAbsoluteBasePlugin(),
electronDesktopHtmlPlugin(),
...(sharedRendererPlugins({ platform: 'desktop' }) as PluginOption[]),
],
+1 -1
View File
@@ -11,7 +11,7 @@
"author": "LobeHub",
"main": "./dist/main/index.js",
"scripts": {
"build:cli": "cd ../cli && bun run build",
"build:cli": "cd ../cli && cross-env MINIFY=1 bun run build",
"build:main": "cross-env NODE_OPTIONS=--max-old-space-size=8192 electron-vite build",
"build:run-unpack": "electron .",
"dev": "electron-vite dev",
+3
View File
@@ -1,8 +1,11 @@
packages:
- '../../packages/const'
- '../../packages/electron-server-ipc'
- '../../packages/electron-client-ipc'
- '../../packages/file-loaders'
- '../../packages/desktop-bridge'
- '../../packages/device-gateway-client'
- '../../packages/local-file-shell'
- './stubs/business-const'
- './stubs/types'
- '.'
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html class="desktop">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html,
body {
margin: 0;
height: 100%;
background: transparent;
}
html[data-theme='dark'] {
background: #141414;
}
html[data-theme='light'] {
background: #fafafa;
}
#loading-screen {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: inherit;
gap: 12px;
}
@keyframes loading-draw {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes loading-fill {
30% {
fill-opacity: 0.05;
}
100% {
fill-opacity: 1;
}
}
#loading-brand {
display: flex;
align-items: center;
gap: 12px;
color: #1f1f1f;
}
#loading-brand svg path {
fill: currentcolor;
fill-opacity: 0;
stroke: currentcolor;
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
stroke-width: 0.25em;
animation:
loading-draw 2s cubic-bezier(0.4, 0, 0.2, 1) infinite,
loading-fill 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
html[data-theme='dark'] #loading-brand {
color: #f0f0f0;
}
</style>
</head>
<body>
<script>
(function () {
var theme = 'system';
try {
theme = localStorage.getItem('theme') || 'system';
} catch (_) {}
var systemTheme =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
var resolvedTheme = theme === 'system' ? systemTheme : theme;
if (resolvedTheme === 'dark' || resolvedTheme === 'light') {
document.documentElement.setAttribute('data-theme', resolvedTheme);
}
var urlParams = new URLSearchParams(window.location.search);
var locale = urlParams.get('lng') || navigator.language || 'en-US';
document.documentElement.lang = locale;
var rtl = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
document.documentElement.dir =
rtl.indexOf(locale.split('-')[0].toLowerCase()) >= 0 ? 'rtl' : 'ltr';
})();
</script>
<div id="loading-screen">
<div id="loading-brand" aria-label="Loading" role="status">
<svg
fill="currentColor"
fill-rule="evenodd"
height="40"
style="flex: none; line-height: 1"
viewBox="0 0 940 320"
xmlns="http://www.w3.org/2000/svg"
>
<title>LobeHub</title>
<path
d="M15 240.035V87.172h39.24V205.75h66.192v34.285H15zM183.731 242c-11.759 0-22.196-2.621-31.313-7.862-9.116-5.241-16.317-12.447-21.601-21.619-5.153-9.317-7.729-19.945-7.729-31.883 0-11.937 2.576-22.492 7.729-31.664 5.164-8.963 12.159-15.98 20.982-21.05l.619-.351c9.117-5.241 19.554-7.861 31.313-7.861s22.196 2.62 31.313 7.861c9.248 5.096 16.449 12.229 21.601 21.401 5.153 9.172 7.729 19.727 7.729 31.664 0 11.938-2.576 22.566-7.729 31.883-5.152 9.172-12.353 16.378-21.601 21.619-9.117 5.241-19.554 7.862-31.313 7.862zm0-32.975c4.36 0 8.191-1.092 11.494-3.275 3.436-2.184 6.144-5.387 8.126-9.609 1.982-4.367 2.973-9.536 2.973-15.505 0-5.968-.991-10.991-2.973-15.067-1.906-4.06-4.483-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.134-3.276-11.494-3.276-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275zM295.508 78l-.001 54.042a34.071 34.071 0 016.541-5.781c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.557 7.424 7.872 4.835 14.105 11.684 18.7 20.546l.325.637c4.756 9.026 7.135 19.799 7.135 32.319 0 12.666-2.379 23.585-7.135 32.757-4.624 9.026-10.966 16.087-19.025 21.182-7.928 4.95-16.78 7.425-26.557 7.425-9.644 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.355-7.532-7.226l.001 11.812h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.494 3.276-3.303 2.184-6.012 5.387-8.126 9.609-1.982 4.076-2.972 9.099-2.972 15.067 0 5.969.99 11.138 2.972 15.505 2.114 4.222 4.823 7.425 8.126 9.609 3.435 2.183 7.266 3.275 11.494 3.275s7.994-1.092 11.297-3.275c3.435-2.184 6.143-5.387 8.125-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.483-7.177-7.732-9.352l-.393-.257c-3.303-2.184-7.069-3.276-11.297-3.276zm105.335 38.653l.084.337a27.857 27.857 0 002.057 5.559c2.246 4.222 5.417 7.498 9.513 9.827 4.096 2.184 8.984 3.276 14.665 3.276 5.285 0 9.777-.801 13.477-2.403 3.579-1.632 7.1-4.025 10.564-7.182l.732-.679 19.818 22.711c-5.153 6.26-11.494 11.064-19.025 14.413-7.531 3.203-16.449 4.804-26.755 4.804-12.683 0-23.782-2.621-33.294-7.862-9.381-5.386-16.713-12.665-21.998-21.837-5.153-9.317-7.729-19.872-7.729-31.665 0-11.792 2.51-22.274 7.53-31.446 5.036-9.105 11.902-16.195 20.596-21.268l.61-.351c8.984-5.241 19.091-7.861 30.322-7.861 10.311 0 19.743 2.286 28.294 6.859l.64.347c8.72 4.659 15.656 11.574 20.809 20.746 5.153 9.172 7.729 20.309 7.729 33.411 0 1.294-.052 2.761-.156 4.4l-.042.623-.17 2.353c-.075 1.01-.151 1.973-.227 2.888h-78.044zm21.365-42.147c-4.492 0-8.456 1.092-11.891 3.276-3.303 2.184-5.879 5.314-7.729 9.39a26.04 26.04 0 00-1.117 2.79 30.164 30.164 0 00-1.121 4.499l-.058.354h43.96l-.015-.106c-.401-2.638-1.122-5.055-2.163-7.252l-.246-.503c-1.776-3.774-4.282-6.742-7.519-8.906l-.409-.266c-3.303-2.184-7.2-3.276-11.692-3.276zm111.695-62.018l-.001 57.432h53.51V87.172h39.24v152.863h-39.24v-59.617H555.9l.001 59.617h-39.24V87.172h39.24zM715.766 242c-8.72 0-16.581-1.893-23.583-5.678-6.87-3.785-12.287-9.681-16.251-17.688-3.832-8.153-5.747-18.417-5.747-30.791v-66.168h37.654v59.398c0 9.172 1.519 15.723 4.558 19.654 3.171 3.931 7.597 5.896 13.278 5.896 3.7 0 7.069-.946 10.108-2.839 3.038-1.892 5.483-4.877 7.332-8.953 1.85-4.222 2.775-9.609 2.775-16.16v-56.996h37.654v118.36h-35.871l.004-12.38c-2.642 3.197-5.682 5.868-9.12 8.012-7.002 4.222-14.599 6.333-22.791 6.333zM841.489 78l-.001 54.041a34.1 34.1 0 016.541-5.78c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.556 7.424 7.873 4.835 14.106 11.684 18.701 20.546l.325.637c4.756 9.026 7.134 19.799 7.134 32.319 0 12.666-2.378 23.585-7.134 32.757-4.624 9.026-10.966 16.087-19.026 21.182-7.927 4.95-16.779 7.425-26.556 7.425-9.645 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.354-7.531-7.224v11.81h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275 4.228 0 7.993-1.092 11.296-3.275 3.435-2.184 6.144-5.387 8.126-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.484-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.068-3.276-11.296-3.276z"
/>
</svg>
</div>
</div>
<div id="root" style="height: 100%"></div>
<script>
window.__SERVER_CONFIG__ = undefined;
</script>
<script type="module" src="/src/spa/entry.popup.tsx"></script>
</body>
</html>
@@ -48,6 +48,7 @@
"file.newAgent": "新建助手",
"file.newAgentGroup": "新建助手组",
"file.newPage": "新建页面",
"file.newTab": "新建标签页",
"file.newTopic": "新建话题",
"file.preferences": "设置…",
"file.quit": "退出",
+14
View File
@@ -66,6 +66,20 @@ export const windowTemplates = {
titleBarStyle: 'hidden',
width: 900,
},
// Dedicated single-topic popup window. Loads the popup.html SPA entry
// (no sidebar / portal), one window per (scope, id) pair.
topicPopup: {
allowMultipleInstances: true,
autoHideMenuBar: true,
baseIdentifier: 'topicPopup',
basePath: '/popup',
height: 720,
keepAlive: false,
minWidth: 480,
parentIdentifier: 'app',
titleBarStyle: 'hidden',
width: 900,
},
} satisfies Record<string, WindowTemplate>;
export type AppBrowsersIdentifiers = keyof typeof appBrowsers;
+2 -2
View File
@@ -1,11 +1,11 @@
/**
* Application settings storage related constants
*/
import { DEFAULT_ELECTRON_DESKTOP_SHORTCUTS } from '@lobechat/const/desktopGlobalShortcuts';
import type { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { appStorageDir } from '@/const/dir';
import { UPDATE_CHANNEL } from '@/modules/updater/configs';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import type { ElectronMainStore } from '@/types/store';
/**
@@ -35,7 +35,7 @@ export const STORE_DEFAULTS: ElectronMainStore = {
gatewayUrl: 'https://device-gateway.lobehub.com',
locale: 'auto',
networkProxy: defaultProxySettings,
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
shortcuts: DEFAULT_ELECTRON_DESKTOP_SHORTCUTS,
storagePath: appStorageDir,
themeMode: 'system',
updateChannel: UPDATE_CHANNEL,
@@ -1,4 +1,5 @@
import type {
FocusTopicPopupParams,
InterceptRouteParams,
OpenSettingsWindowOptions,
WindowMinimumSizeParams,
@@ -80,6 +81,30 @@ export default class BrowserWindowsCtr extends ControllerModule {
});
}
@IpcMethod()
setWindowAlwaysOnTop(flag: boolean) {
this.withSenderIdentifier((identifier) => {
this.app.browserManager.setWindowAlwaysOnTop(identifier, flag);
});
}
@IpcMethod()
isWindowAlwaysOnTop() {
return this.withSenderIdentifier((identifier) => {
return this.app.browserManager.isWindowAlwaysOnTop(identifier);
});
}
@IpcMethod()
listTopicPopups() {
return this.app.browserManager.listTopicPopups();
}
@IpcMethod()
focusTopicPopup(params: FocusTopicPopupParams) {
return this.app.browserManager.focusTopicPopup(params.identifier);
}
@IpcMethod()
setWindowSize(params: WindowSizeParams) {
this.withSenderIdentifier((identifier) => {
+325
View File
@@ -0,0 +1,325 @@
import { execFile } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import type {
GitAheadBehind,
GitBranchInfo,
GitBranchListItem,
GitCheckoutResult,
GitLinkedPullRequestResult,
GitWorkingTreeFiles,
GitWorkingTreeStatus,
} from '@lobechat/electron-client-ipc';
import { detectRepoType, resolveGitDir } from '@/utils/git';
import { createLogger } from '@/utils/logger';
import { ControllerModule, IpcMethod } from './index';
const logger = createLogger('controllers:GitCtr');
export default class GitController extends ControllerModule {
static override readonly groupName = 'git';
@IpcMethod()
async detectRepoType(dirPath: string): Promise<'git' | 'github' | undefined> {
return detectRepoType(dirPath);
}
/**
* Read current git branch from `.git/HEAD`. Returns short sha on detached HEAD.
* Handles both standard `.git` directories and `.git` worktree pointer files.
*/
@IpcMethod()
async getGitBranch(dirPath: string): Promise<GitBranchInfo> {
try {
const gitDir = await resolveGitDir(dirPath);
if (!gitDir) return {};
const head = (await readFile(path.join(gitDir, 'HEAD'), 'utf8')).trim();
const refMatch = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
if (refMatch) {
return { branch: refMatch[1] };
}
// Detached HEAD — HEAD file contains the full sha
if (/^[\da-f]{40}$/i.test(head)) {
return { branch: head.slice(0, 7), detached: true };
}
return {};
} catch {
return {};
}
}
/**
* Query `gh` CLI for an open pull request whose head branch matches `branch`.
* Returns status = 'gh-missing' when `gh` is not installed / not authenticated,
* so the UI can render a helpful tooltip instead of an error.
*/
@IpcMethod()
async getLinkedPullRequest(payload: {
branch: string;
path: string;
}): Promise<GitLinkedPullRequestResult> {
const { path: dirPath, branch } = payload;
if (!branch) {
return { pullRequest: null, status: 'ok' };
}
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync(
'gh',
[
'pr',
'list',
'--head',
branch,
'--state',
'open',
'--limit',
'5',
'--json',
'number,url,title,state',
],
{ cwd: dirPath, timeout: 8000 },
);
const parsed = JSON.parse(stdout.trim() || '[]') as Array<{
number: number;
state: string;
title: string;
url: string;
}>;
if (parsed.length === 0) {
return { pullRequest: null, status: 'ok' };
}
const [primary, ...rest] = parsed;
return {
extraCount: rest.length,
pullRequest: primary,
status: 'ok',
};
} catch (error: any) {
const code = error?.code;
const stderr: string = error?.stderr ?? '';
// `gh` binary not on PATH
if (code === 'ENOENT') {
return { pullRequest: null, status: 'gh-missing' };
}
// gh reports auth issues via stderr; treat as a soft-fail
if (/auth\s+login|not\s+logged\s+in|authentication/i.test(stderr)) {
return { pullRequest: null, status: 'gh-missing' };
}
logger.debug('[getLinkedPullRequest] failed', { branch, code, stderr });
return { pullRequest: null, status: 'error' };
}
}
/**
* List local git branches ordered by most recent commit.
* `current` is true for the checked-out branch.
*/
@IpcMethod()
async listGitBranches(dirPath: string): Promise<GitBranchListItem[]> {
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync(
'git',
[
'for-each-ref',
'--sort=-committerdate',
'--format=%(HEAD)%09%(refname:short)%09%(upstream:short)',
'refs/heads',
],
{ cwd: dirPath, timeout: 5000 },
);
return stdout
.replaceAll('\r', '')
.split('\n')
.filter((line) => line.length > 0)
.map((line) => {
// Line format: "<HEAD-marker>\t<branch>\t<upstream>" where HEAD-marker is '*' or ' '
const [head, name, upstream] = line.split('\t');
return {
current: head === '*',
name: name ?? '',
upstream: upstream || undefined,
};
})
.filter((b) => b.name);
} catch (error: any) {
logger.warn('[listGitBranches] git command failed', {
code: error?.code,
cwd: dirPath,
message: error?.message,
stderr: error?.stderr?.toString?.() ?? error?.stderr,
});
return [];
}
}
/**
* Bucket dirty files into added / modified / deleted via `git status --porcelain -z`.
* Each file is counted once: untracked (`??`) and staged-add (`A`) → added,
* any `D` in index or working tree → deleted, everything else (`M`/`R`/`C`/`T`/`U`) → modified.
*
* Uses `-z` so paths are NUL-terminated (no C-style quoting, no `\n` splitting bugs).
* Rename/copy entries (`R`/`C`) emit two NUL-separated tokens — dest path then source
* path — so the source token must be consumed to keep counts correct.
*/
@IpcMethod()
async getGitWorkingTreeStatus(dirPath: string): Promise<GitWorkingTreeStatus> {
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
cwd: dirPath,
timeout: 5000,
});
const tokens = stdout.split('\0');
let added = 0;
let modified = 0;
let deleted = 0;
let i = 0;
while (i < tokens.length) {
const entry = tokens[i];
i++;
if (entry.length < 2) continue;
const x = entry[0];
const y = entry[1];
// R/C entries carry an extra source-path token we must consume.
if (x === 'R' || x === 'C') i++;
if (x === '?' && y === '?') {
added++;
} else if (x === '!' && y === '!') {
// ignored — skip
} else if (x === 'D' || y === 'D') {
deleted++;
} else if (x === 'A' || y === 'A') {
added++;
} else {
modified++;
}
}
const total = added + modified + deleted;
return { added, clean: total === 0, deleted, modified, total };
} catch {
return { added: 0, clean: true, deleted: 0, modified: 0, total: 0 };
}
}
/**
* Return dirty file paths bucketed into added / modified / deleted.
* Same classification as getGitWorkingTreeStatus, but with per-file paths.
*
* Uses `git status --porcelain -z` so paths are NUL-terminated and never C-quoted,
* which avoids misparsing filenames that legitimately contain ` -> `, quote chars,
* or newlines. For R/C entries the two NUL-separated tokens are `DEST\0SRC`; we
* report DEST (the current working-tree path) and discard SRC.
*/
@IpcMethod()
async getGitWorkingTreeFiles(dirPath: string): Promise<GitWorkingTreeFiles> {
const execFileAsync = promisify(execFile);
const added: string[] = [];
const modified: string[] = [];
const deleted: string[] = [];
try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-z'], {
cwd: dirPath,
timeout: 5000,
});
const tokens = stdout.split('\0');
let i = 0;
while (i < tokens.length) {
const entry = tokens[i];
i++;
if (entry.length < 3) continue;
const x = entry[0];
const y = entry[1];
const filePath = entry.slice(3);
// R/C entries carry an extra source-path token we must consume.
if (x === 'R' || x === 'C') i++;
if (!filePath) continue;
if (x === '?' && y === '?') {
added.push(filePath);
} else if (x === '!' && y === '!') {
// ignored — skip
} else if (x === 'D' || y === 'D') {
deleted.push(filePath);
} else if (x === 'A' || y === 'A') {
added.push(filePath);
} else {
modified.push(filePath);
}
}
return { added, deleted, modified };
} catch {
return { added: [], deleted: [], modified: [] };
}
}
/**
* Count commits HEAD is ahead/behind its upstream tracking ref.
* Returns `hasUpstream: false` when the branch has no upstream configured
* (e.g. local-only branches, or after the remote branch is deleted).
*/
@IpcMethod()
async getGitAheadBehind(dirPath: string): Promise<GitAheadBehind> {
const execFileAsync = promisify(execFile);
try {
const { stdout: upstreamOut } = await execFileAsync(
'git',
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
{ cwd: dirPath, timeout: 5000 },
);
const upstream = upstreamOut.trim();
if (!upstream) return { ahead: 0, behind: 0, hasUpstream: false };
const { stdout } = await execFileAsync(
'git',
['rev-list', '--left-right', '--count', `${upstream}...HEAD`],
{ cwd: dirPath, timeout: 5000 },
);
const [behindStr, aheadStr] = stdout.trim().split(/\s+/);
const behind = Number.parseInt(behindStr ?? '0', 10) || 0;
const ahead = Number.parseInt(aheadStr ?? '0', 10) || 0;
return { ahead, behind, hasUpstream: true, upstream };
} catch {
// No upstream configured, detached HEAD, or git error — all treated as "no upstream"
return { ahead: 0, behind: 0, hasUpstream: false };
}
}
/**
* Check out (or create + check out) a branch.
* Relies on git itself to reject unsafe checkouts (dirty tree, non-fast-forward, etc.)
* and surfaces git's stderr so the UI can display a meaningful error.
*/
@IpcMethod()
async checkoutGitBranch(payload: {
branch: string;
create?: boolean;
path: string;
}): Promise<GitCheckoutResult> {
const { path: dirPath, branch, create } = payload;
if (!branch?.trim()) {
return { error: 'Branch name is required', success: false };
}
// Reject obviously invalid refs early to avoid a confusing git error
if (/[\s~^:?*[\\]/.test(branch) || branch.startsWith('-') || branch.includes('..')) {
return { error: `Invalid branch name: ${branch}`, success: false };
}
const execFileAsync = promisify(execFile);
const args = create ? ['checkout', '-b', branch] : ['checkout', branch];
try {
await execFileAsync('git', args, { cwd: dirPath, timeout: 10_000 });
return { success: true };
} catch (error: any) {
const stderr: string = (error?.stderr ?? error?.message ?? '').toString().trim();
logger.debug('[checkoutGitBranch] failed', { args, stderr });
return { error: stderr || 'git checkout failed', success: false };
}
}
}
@@ -0,0 +1,515 @@
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import type { Readable, Writable } from 'node:stream';
import { app as electronApp, BrowserWindow } from 'electron';
import { buildProxyEnv } from '@/modules/networkProxy/envBuilder';
import { createLogger } from '@/utils/logger';
import { ControllerModule, IpcMethod } from './index';
const logger = createLogger('controllers:HeterogeneousAgentCtr');
/** Directory under appStoragePath for caching downloaded files */
const FILE_CACHE_DIR = 'heteroAgent/files';
// ─── CLI presets per agent type ───
// Mirrors @lobechat/heterogeneous-agents/registry but runs in main process
// (can't import from the workspace package in Electron main directly)
interface CLIPreset {
baseArgs: string[];
promptMode: 'positional' | 'stdin';
resumeArgs?: (sessionId: string) => string[];
}
const CLI_PRESETS: Record<string, CLIPreset> = {
'claude-code': {
baseArgs: [
'-p',
'--input-format',
'stream-json',
'--output-format',
'stream-json',
'--verbose',
'--include-partial-messages',
'--permission-mode',
'bypassPermissions',
],
promptMode: 'stdin',
resumeArgs: (sid) => ['--resume', sid],
},
// Future presets:
// 'codex': { baseArgs: [...], promptMode: 'positional' },
// 'kimi-cli': { baseArgs: [...], promptMode: 'positional' },
};
// ─── IPC types ───
interface StartSessionParams {
/** Agent type key (e.g., 'claude-code'). Defaults to 'claude-code'. */
agentType?: string;
/** Additional CLI arguments */
args?: string[];
/** Command to execute */
command: string;
/** Working directory */
cwd?: string;
/** Environment variables */
env?: Record<string, string>;
/** Session ID to resume (for multi-turn) */
resumeSessionId?: string;
}
interface StartSessionResult {
sessionId: string;
}
interface ImageAttachment {
id: string;
url: string;
}
interface SendPromptParams {
/** Image attachments to include in the prompt (downloaded from url, cached by id) */
imageList?: ImageAttachment[];
prompt: string;
sessionId: string;
}
interface CancelSessionParams {
sessionId: string;
}
interface StopSessionParams {
sessionId: string;
}
interface GetSessionInfoParams {
sessionId: string;
}
interface SessionInfo {
agentSessionId?: string;
}
// ─── Internal session tracking ───
interface AgentSession {
agentSessionId?: string;
agentType: string;
args: string[];
/**
* True when *we* initiated the kill (cancelSession / stopSession / before-quit).
* The `exit` handler uses this to route signal-induced non-zero exits through
* the `complete` broadcast instead of surfacing them as runtime errors —
* SIGINT(130) / SIGTERM(143) / SIGKILL(137) from our own kill paths are
* intentional, not agent failures.
*/
cancelledByUs?: boolean;
command: string;
cwd?: string;
env?: Record<string, string>;
process?: ChildProcess;
sessionId: string;
}
/**
* External Agent Controller — manages external agent CLI processes via Electron IPC.
*
* Agent-agnostic: uses CLI presets from a registry to support Claude Code,
* Codex, Kimi CLI, etc. Only handles process lifecycle and raw stdout line
* broadcasting. All event parsing and DB persistence happens on the Renderer side.
*
* Lifecycle: startSession → sendPrompt → (heteroAgentRawLine broadcasts) → stopSession
*/
export default class HeterogeneousAgentCtr extends ControllerModule {
static override readonly groupName = 'heterogeneousAgent';
private sessions = new Map<string, AgentSession>();
// ─── Broadcast ───
private broadcast<T>(channel: string, data: T) {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) {
win.webContents.send(channel, data);
}
}
}
// ─── File cache ───
private get fileCacheDir(): string {
return path.join(this.app.appStoragePath, FILE_CACHE_DIR);
}
/**
* Derive a filesystem-safe cache key for attachments.
*
* Never use the raw image id as a path segment — upstream callers can persist
* arbitrary ids and path.join would treat traversal sequences as real
* directories. A stable hash preserves cache hits without trusting the id as a
* filename.
*/
private getImageCacheKey(imageId: string): string {
return createHash('sha256').update(imageId).digest('hex');
}
/**
* Download an image by URL, with local disk cache keyed by id.
*/
private async resolveImage(
image: ImageAttachment,
): Promise<{ buffer: Buffer; mimeType: string }> {
const cacheDir = this.fileCacheDir;
const cacheKey = this.getImageCacheKey(image.id);
const metaPath = path.join(cacheDir, `${cacheKey}.meta`);
const dataPath = path.join(cacheDir, cacheKey);
// Check cache first
try {
const metaRaw = await readFile(metaPath, 'utf8');
const meta = JSON.parse(metaRaw);
const buffer = await readFile(dataPath);
logger.debug('Image cache hit:', image.id);
return { buffer, mimeType: meta.mimeType || 'image/png' };
} catch {
// Cache miss — download
}
logger.info('Downloading image:', image.id);
const res = await fetch(image.url);
if (!res.ok)
throw new Error(`Failed to download image ${image.id}: ${res.status} ${res.statusText}`);
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const mimeType = res.headers.get('content-type') || 'image/png';
// Write to cache
await mkdir(cacheDir, { recursive: true });
await writeFile(dataPath, buffer);
await writeFile(metaPath, JSON.stringify({ id: image.id, mimeType }));
logger.debug('Image cached:', image.id, `${buffer.length} bytes`);
return { buffer, mimeType };
}
/**
* Build a stream-json user message with text + optional image content blocks.
*/
private async buildStreamJsonInput(
prompt: string,
imageList: ImageAttachment[] = [],
): Promise<string> {
const content: any[] = [{ text: prompt, type: 'text' }];
for (const image of imageList) {
try {
const { buffer, mimeType } = await this.resolveImage(image);
content.push({
source: {
data: buffer.toString('base64'),
media_type: mimeType,
type: 'base64',
},
type: 'image',
});
} catch (err) {
logger.error(`Failed to resolve image ${image.id}:`, err);
}
}
return JSON.stringify({
message: { content, role: 'user' },
type: 'user',
});
}
// ─── IPC methods ───
/**
* Create a session (stores config, process spawned on sendPrompt).
*/
@IpcMethod()
async startSession(params: StartSessionParams): Promise<StartSessionResult> {
const sessionId = randomUUID();
const agentType = params.agentType || 'claude-code';
this.sessions.set(sessionId, {
// If resuming, pre-set the agent session ID so sendPrompt adds --resume
agentSessionId: params.resumeSessionId,
agentType,
args: params.args || [],
command: params.command,
cwd: params.cwd,
env: params.env,
sessionId,
});
logger.info('Session created:', { agentType, sessionId });
return { sessionId };
}
/**
* Send a prompt to an agent session.
*
* Spawns the CLI process with preset flags. Broadcasts each stdout line
* as an `heteroAgentRawLine` event — Renderer side parses and adapts.
*/
@IpcMethod()
async sendPrompt(params: SendPromptParams): Promise<void> {
const session = this.sessions.get(params.sessionId);
if (!session) throw new Error(`Session not found: ${params.sessionId}`);
const preset = CLI_PRESETS[session.agentType];
if (!preset) throw new Error(`Unknown agent type: ${session.agentType}`);
const useStdin = preset.promptMode === 'stdin';
// Build stream-json payload up-front so any image download errors
// surface before the process is spawned.
let stdinPayload: string | undefined;
if (useStdin) {
stdinPayload = await this.buildStreamJsonInput(params.prompt, params.imageList ?? []);
}
return new Promise<void>((resolve, reject) => {
// Build CLI args: base preset + resume + user args
const cliArgs = [
...preset.baseArgs,
...(session.agentSessionId && preset.resumeArgs
? preset.resumeArgs(session.agentSessionId)
: []),
...session.args,
];
if (!useStdin && preset.promptMode === 'positional') {
// Positional mode: append prompt as a CLI arg (legacy / non-CC presets).
cliArgs.push(params.prompt);
}
// Fall back to the user's Desktop so the process never inherits
// the Electron parent's cwd (which is `/` when launched from Finder).
const cwd = session.cwd || electronApp.getPath('desktop');
logger.info('Spawning agent:', session.command, cliArgs.join(' '), `(cwd: ${cwd})`);
// `detached: true` on Unix puts the child in a new process group so we
// can SIGINT/SIGKILL the whole tree (claude + any tool subprocesses)
// via `process.kill(-pid, sig)` on cancel. Without this, SIGINT to just
// the claude binary can leave bash/grep/etc. tool children running and
// the CLI hung waiting on them. Windows has different semantics — use
// taskkill /T /F there; no detached flag needed.
// Forward the user's proxy settings to the CLI. The main-process undici
// dispatcher doesn't reach child processes — they need env vars.
const proxyEnv = buildProxyEnv(this.app.storeManager.get('networkProxy'));
const proc = spawn(session.command, cliArgs, {
cwd,
detached: process.platform !== 'win32',
env: { ...process.env, ...proxyEnv, ...session.env },
stdio: [useStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
});
// In stdin mode, write the stream-json message and close stdin.
if (useStdin && stdinPayload && proc.stdin) {
const stdin = proc.stdin as Writable;
stdin.write(stdinPayload + '\n', () => {
stdin.end();
});
}
session.process = proc;
let buffer = '';
// Stream stdout lines as raw events to Renderer
const stdout = proc.stdout as Readable;
stdout.on('data', (chunk: Buffer) => {
buffer += chunk.toString('utf8');
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
// Extract agent session ID from init event (for multi-turn)
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
session.agentSessionId = parsed.session_id;
}
// Broadcast raw parsed JSON — Renderer handles all adaptation
this.broadcast('heteroAgentRawLine', {
line: parsed,
sessionId: session.sessionId,
});
} catch {
// Not valid JSON, skip
}
}
});
// Capture stderr
const stderrChunks: string[] = [];
const stderr = proc.stderr as Readable;
stderr.on('data', (chunk: Buffer) => {
stderrChunks.push(chunk.toString('utf8'));
});
proc.on('error', (err) => {
logger.error('Agent process error:', err);
this.broadcast('heteroAgentSessionError', {
error: err.message,
sessionId: session.sessionId,
});
reject(err);
});
proc.on('exit', (code, signal) => {
logger.info('Agent process exited:', { code, sessionId: session.sessionId, signal });
session.process = undefined;
// If *we* killed it (cancel / stop / before-quit), treat the non-zero
// exit as a clean shutdown — surfacing it as an error would make a
// user-initiated cancel look like an agent failure, and an Electron
// shutdown affecting OTHER running CC sessions would pollute their
// topics with a misleading "Agent exited with code 143" message.
if (session.cancelledByUs) {
this.broadcast('heteroAgentSessionComplete', { sessionId: session.sessionId });
resolve();
return;
}
if (code === 0) {
this.broadcast('heteroAgentSessionComplete', { sessionId: session.sessionId });
resolve();
} else {
const stderrOutput = stderrChunks.join('').trim();
const errorMsg = stderrOutput || `Agent exited with code ${code}`;
this.broadcast('heteroAgentSessionError', {
error: errorMsg,
sessionId: session.sessionId,
});
reject(new Error(errorMsg));
}
});
});
}
/**
* Get session info (agent's internal session ID for multi-turn resume).
*/
@IpcMethod()
async getSessionInfo(params: GetSessionInfoParams): Promise<SessionInfo> {
const session = this.sessions.get(params.sessionId);
return { agentSessionId: session?.agentSessionId };
}
/**
* Signal the whole process tree spawned by this session.
*
* On Unix the child was spawned with `detached: true`, so negating the pid
* signals the process group — reaching tool subprocesses (bash, grep, etc.)
* that would otherwise orphan after a parent-only kill. Falls back to the
* direct signal if the group kill raises (ESRCH when the leader is already
* gone). On Windows we shell out to `taskkill /T /F` which walks the tree.
*/
private killProcessTree(proc: ChildProcess, signal: NodeJS.Signals): void {
if (!proc.pid || proc.killed) return;
if (process.platform === 'win32') {
try {
spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { stdio: 'ignore' });
} catch (err) {
logger.warn('taskkill failed:', err);
}
return;
}
try {
process.kill(-proc.pid, signal);
} catch {
try {
proc.kill(signal);
} catch {
// already exited
}
}
}
/**
* Cancel an ongoing session: SIGINT the CC tree, escalate to SIGKILL after
* 2s if the CLI hasn't exited (some tool calls swallow SIGINT). The
* `exit` handler on the spawned proc broadcasts completion and clears
* `session.process`, so the escalation is a no-op when the graceful path
* already landed.
*/
@IpcMethod()
async cancelSession(params: CancelSessionParams): Promise<void> {
const session = this.sessions.get(params.sessionId);
if (!session?.process || session.process.killed) return;
session.cancelledByUs = true;
const proc = session.process;
this.killProcessTree(proc, 'SIGINT');
setTimeout(() => {
if (session.process === proc && !proc.killed) {
logger.warn('Session did not exit after SIGINT, escalating to SIGKILL:', params.sessionId);
this.killProcessTree(proc, 'SIGKILL');
}
}, 2000);
}
/**
* Stop and clean up a session.
*/
@IpcMethod()
async stopSession(params: StopSessionParams): Promise<void> {
const session = this.sessions.get(params.sessionId);
if (!session) return;
if (session.process && !session.process.killed) {
session.cancelledByUs = true;
const proc = session.process;
this.killProcessTree(proc, 'SIGTERM');
setTimeout(() => {
if (session.process === proc && !proc.killed) {
this.killProcessTree(proc, 'SIGKILL');
}
}, 3000);
}
this.sessions.delete(params.sessionId);
}
@IpcMethod()
async respondPermission(): Promise<void> {
// No-op for CLI mode (permissions handled by --permission-mode flag)
}
/**
* Cleanup on app quit.
*/
afterAppReady() {
electronApp.on('before-quit', () => {
for (const [, session] of this.sessions) {
if (session.process && !session.process.killed) {
session.cancelledByUs = true;
this.killProcessTree(session.process, 'SIGTERM');
}
}
this.sessions.clear();
});
}
}
@@ -178,6 +178,28 @@ export default class NotificationCtr extends ControllerModule {
}
}
/**
* Set the app-level badge count (dock red dot on macOS, Unity counter on Linux,
* overlay icon on Windows). Pass 0 to clear.
*
* On macOS we pair `app.setBadgeCount` with `app.dock.setBadge` — the former
* keeps Electron's internal count (cross-platform), the latter is the
* reliable Dock repaint trigger. Note: macOS Focus Mode / DND suppresses the
* badge visually until the user exits Focus.
*/
@IpcMethod()
setBadgeCount(count: number): void {
try {
const next = Math.max(0, Math.floor(count));
app.setBadgeCount(next);
if (macOS() && app.dock) {
app.dock.setBadge(next > 0 ? String(next) : '');
}
} catch (error) {
logger.error('Failed to set badge count:', error);
}
}
/**
* Check if the main window is hidden
*/
+2 -14
View File
@@ -1,5 +1,3 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import type { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
@@ -8,6 +6,7 @@ import { macOS } from 'electron-is';
import { pathExists, readdir } from 'fs-extra';
import { legacyLocalDbDir } from '@/const/dir';
import { detectRepoType } from '@/utils/git';
import { createLogger } from '@/utils/logger';
import {
getAccessibilityStatus,
@@ -185,7 +184,7 @@ export default class SystemController extends ControllerModule {
}
const folderPath = result.filePaths[0];
const repoType = await this.detectRepoType(folderPath);
const repoType = await detectRepoType(folderPath);
return { path: folderPath, repoType };
}
@@ -235,17 +234,6 @@ export default class SystemController extends ControllerModule {
}
}
private async detectRepoType(dirPath: string): Promise<'git' | 'github' | undefined> {
const gitConfigPath = path.join(dirPath, '.git', 'config');
try {
const config = await readFile(gitConfigPath, 'utf8');
if (config.includes('github.com')) return 'github';
return 'git';
} catch {
return undefined;
}
}
private async setSystemThemeMode(themeMode: ThemeMode) {
nativeTheme.themeSource = themeMode;
}
@@ -1,8 +1,15 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import type { ClaudeAuthStatus } from '@lobechat/electron-client-ipc';
import type { ToolCategory, ToolStatus } from '@/core/infrastructure/ToolDetectorManager';
import { createLogger } from '@/utils/logger';
import { ControllerModule, IpcMethod } from './index';
const execPromise = promisify(exec);
const logger = createLogger('controllers:ToolDetectorCtr');
/**
@@ -112,4 +119,19 @@ export default class ToolDetectorCtr extends ControllerModule {
priority: detector.priority,
}));
}
/**
* Get Claude Code CLI auth/account status by running `claude auth status --json`.
* Returns null if the CLI is unavailable or the command fails.
*/
@IpcMethod()
async getClaudeAuthStatus(): Promise<ClaudeAuthStatus | null> {
try {
const { stdout } = await execPromise('claude auth status --json', { timeout: 5000 });
return JSON.parse(stdout.trim()) as ClaudeAuthStatus;
} catch (error) {
logger.debug('Failed to get claude auth status:', error);
return null;
}
}
}
@@ -19,7 +19,7 @@ vi.mock('electron', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockToggleVisible = vi.fn();
const mockLoadUrl = vi.fn();
const mockShow = vi.fn();
@@ -14,29 +14,29 @@ vi.mock('electron', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockShow = vi.fn();
const mockRetrieveByIdentifier = vi.fn(() => ({
show: mockShow,
}));
// 创建一个足够模拟 App 行为的对象,以满足 DevtoolsCtr 的需求
// Create an object that sufficiently mocks App behavior to satisfy DevtoolsCtr's needs
const mockApp = {
browserManager: {
retrieveByIdentifier: mockRetrieveByIdentifier,
},
// 如果 DevtoolsCtr 或其基类在构造或方法调用中使用了 app 的其他属性/方法,
// 也需要在这里添加相应的模拟
} as unknown as App; // 使用类型断言,因为我们只模拟了部分 App 结构
// If DevtoolsCtr or its base class uses other app properties/methods during construction or method calls,
// they also need to be added as mocks here
} as unknown as App; // Type assertion since we only mock a subset of the App structure
describe('DevtoolsCtr', () => {
let devtoolsCtr: DevtoolsCtr;
beforeEach(() => {
vi.clearAllMocks(); // 只清除 vi.fn() 创建的模拟函数的记录,不影响 IoCContainer 状态
vi.clearAllMocks(); // Only clears mock function records created by vi.fn(), does not affect IoCContainer state
ipcMainHandleMock.mockClear();
// 实例化 DevtoolsCtr。其 @IpcMethod 装饰器会执行并与真实的 IoCContainer 交互。
// Instantiate DevtoolsCtr. Its @IpcMethod decorator will execute and interact with the real IoCContainer.
devtoolsCtr = new DevtoolsCtr(mockApp);
});
@@ -44,9 +44,9 @@ describe('DevtoolsCtr', () => {
it('should retrieve the devtools browser window using app.browserManager and show it', async () => {
await devtoolsCtr.openDevtools();
// 验证 browserManager.retrieveByIdentifier 是否以 'devtools' 参数被调用
// Verify that browserManager.retrieveByIdentifier is called with the 'devtools' argument
expect(mockRetrieveByIdentifier).toHaveBeenCalledWith('devtools');
// 验证返回对象的 show 方法是否被调用
// Verify that the show method of the returned object is called
expect(mockShow).toHaveBeenCalled();
});
});
@@ -0,0 +1,225 @@
import { EventEmitter } from 'node:events';
import { access, mkdtemp, readdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { PassThrough } from 'node:stream';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import HeterogeneousAgentCtr from '../HeterogeneousAgentCtr';
const FAKE_DESKTOP_PATH = '/Users/fake/Desktop';
vi.mock('electron', () => ({
BrowserWindow: { getAllWindows: () => [] },
app: {
getPath: vi.fn((name: string) => (name === 'desktop' ? FAKE_DESKTOP_PATH : `/fake/${name}`)),
on: vi.fn(),
},
ipcMain: { handle: vi.fn() },
}));
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
verbose: vi.fn(),
warn: vi.fn(),
}),
}));
// Captures the most recent spawn() call so sendPrompt tests can assert on argv.
const spawnCalls: Array<{ args: string[]; command: string; options: any }> = [];
let nextFakeProc: any = null;
vi.mock('node:child_process', () => ({
spawn: (command: string, args: string[], options: any) => {
spawnCalls.push({ args, command, options });
return nextFakeProc;
},
}));
/**
* Build a fake ChildProcess that immediately exits cleanly. Records every
* stdin write on the returned `writes` array so tests can inspect the payload.
*/
const createFakeProc = () => {
const proc = new EventEmitter() as any;
const stdout = new PassThrough();
const stderr = new PassThrough();
const writes: string[] = [];
proc.stdout = stdout;
proc.stderr = stderr;
proc.stdin = {
end: vi.fn(),
write: vi.fn((chunk: string, cb?: () => void) => {
writes.push(chunk);
cb?.();
return true;
}),
};
proc.kill = vi.fn();
proc.killed = false;
// Exit asynchronously so the Promise returned by sendPrompt resolves cleanly.
setImmediate(() => {
stdout.end();
stderr.end();
proc.emit('exit', 0);
});
return { proc, writes };
};
describe('HeterogeneousAgentCtr', () => {
let appStoragePath: string;
beforeEach(async () => {
appStoragePath = await mkdtemp(path.join(tmpdir(), 'lobehub-hetero-'));
});
afterEach(async () => {
await rm(appStoragePath, { force: true, recursive: true });
});
describe('resolveImage', () => {
it('stores traversal-looking ids inside the cache root via a stable hash key', async () => {
const ctr = new HeterogeneousAgentCtr({
appStoragePath,
storeManager: { get: vi.fn() },
} as any);
const cacheDir = path.join(appStoragePath, 'heteroAgent/files');
const escapedTargetName = `${path.basename(appStoragePath)}-outside-storage`;
const escapePath = path.join(cacheDir, `../../../${escapedTargetName}`);
try {
await unlink(escapePath);
} catch {
// best-effort cleanup
}
await (ctr as any).resolveImage({
id: `../../../${escapedTargetName}`,
url: 'data:text/plain;base64,T1VUU0lERQ==',
});
const cacheEntries = await readdir(cacheDir);
expect(cacheEntries).toHaveLength(2);
expect(cacheEntries.every((entry) => /^[a-f0-9]{64}(?:\.meta)?$/.test(entry))).toBe(true);
await expect(access(escapePath)).rejects.toThrow();
try {
await unlink(escapePath);
} catch {
// best-effort cleanup
}
});
it('does not trust pre-seeded out-of-root traversal cache files as cache hits', async () => {
const ctr = new HeterogeneousAgentCtr({
appStoragePath,
storeManager: { get: vi.fn() },
} as any);
const cacheDir = path.join(appStoragePath, 'heteroAgent/files');
const traversalId = '../../preexisting-secret';
const outOfRootDataPath = path.join(cacheDir, traversalId);
const outOfRootMetaPath = path.join(cacheDir, `${traversalId}.meta`);
await writeFile(outOfRootDataPath, 'SECRET');
await writeFile(
outOfRootMetaPath,
JSON.stringify({ id: traversalId, mimeType: 'text/plain' }),
);
const result = await (ctr as any).resolveImage({
id: traversalId,
url: 'data:text/plain;base64,SUdOT1JFRA==',
});
expect(Buffer.from(result.buffer).toString('utf8')).toBe('IGNORED');
expect(result.mimeType).toBe('text/plain');
await expect(readFile(outOfRootDataPath, 'utf8')).resolves.toBe('SECRET');
});
});
describe('sendPrompt (claude-code)', () => {
beforeEach(() => {
spawnCalls.length = 0;
});
const runSendPrompt = async (prompt: string, sessionOverrides: Record<string, any> = {}) => {
const { proc, writes } = createFakeProc();
nextFakeProc = proc;
const ctr = new HeterogeneousAgentCtr({
appStoragePath,
storeManager: { get: vi.fn() },
} as any);
const { sessionId } = await ctr.startSession({
agentType: 'claude-code',
command: 'claude',
...sessionOverrides,
});
await ctr.sendPrompt({ prompt, sessionId });
const { args: cliArgs, command, options } = spawnCalls[0];
return { cliArgs, command, options, writes };
};
it('passes prompt via stdin stream-json — never as a positional arg', async () => {
const prompt = '-- 这是破折号测试 --help';
const { cliArgs, writes } = await runSendPrompt(prompt);
// Prompt must never appear in argv (that is what previously broke CC's arg parser).
expect(cliArgs).not.toContain(prompt);
// Stream-json input must be wired up.
expect(cliArgs).toContain('--input-format');
expect(cliArgs).toContain('--output-format');
expect(cliArgs.filter((a) => a === 'stream-json')).toHaveLength(2);
// Exactly one stdin write, carrying the prompt as a user message JSON line.
expect(writes).toHaveLength(1);
const line = writes[0].trimEnd();
expect(line.endsWith('\n') || writes[0].endsWith('\n')).toBe(true);
const msg = JSON.parse(line);
expect(msg).toMatchObject({
message: {
content: [{ text: prompt, type: 'text' }],
role: 'user',
},
type: 'user',
});
});
it.each([
'-flag-looking-prompt',
'--help please',
'- dash at start',
'-p -- mixed',
'normal prompt with -dash- inside',
])('accepts dash-containing prompt without leaking to argv: %s', async (prompt) => {
const { cliArgs, writes } = await runSendPrompt(prompt);
expect(cliArgs).not.toContain(prompt);
expect(writes).toHaveLength(1);
const msg = JSON.parse(writes[0].trimEnd());
expect(msg.message.content[0].text).toBe(prompt);
});
it('falls back to the user Desktop when no cwd is supplied', async () => {
const { options } = await runSendPrompt('hello');
// When launched from Finder the Electron parent cwd is `/` — the
// controller must override that with the user's Desktop so CC writes
// land somewhere sensible.
expect(options.cwd).toBe(FAKE_DESKTOP_PATH);
});
it('respects an explicit cwd passed to startSession', async () => {
const explicitCwd = '/Users/fake/projects/my-repo';
const { options } = await runSendPrompt('hello', { cwd: explicitCwd });
expect(options.cwd).toBe(explicitCwd);
});
});
});
@@ -14,7 +14,7 @@ vi.mock('electron', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockRefreshMenus = vi.fn();
const mockShowContextMenu = vi.fn();
const mockRebuildAppMenu = vi.fn();
@@ -37,7 +37,7 @@ describe('MenuController', () => {
describe('refreshAppMenu', () => {
it('should call menuManager.refreshMenus', () => {
// 模拟返回值
// Mock return value
mockRefreshMenus.mockReturnValueOnce(true);
const result = menuController.refreshAppMenu();
@@ -9,7 +9,7 @@ const { ipcMainHandleMock } = vi.hoisted(() => ({
ipcMainHandleMock: vi.fn(),
}));
// 模拟 logger
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
@@ -19,7 +19,7 @@ vi.mock('@/utils/logger', () => ({
}),
}));
// 模拟 undici - 使用 vi.fn() 直接在 Mock 中创建
// Mock undici - create mocks directly using vi.fn()
vi.mock('undici', () => ({
fetch: vi.fn(),
getGlobalDispatcher: vi.fn(),
@@ -28,7 +28,7 @@ vi.mock('undici', () => ({
ProxyAgent: vi.fn(),
}));
// 模拟 defaultProxySettings
// Mock defaultProxySettings
vi.mock('@/const/store', () => ({
defaultProxySettings: {
enableProxy: false,
@@ -40,7 +40,7 @@ vi.mock('@/const/store', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockStoreManager = {
get: vi.fn(),
set: vi.fn(),
@@ -53,19 +53,19 @@ const mockApp = {
describe('NetworkProxyCtr', () => {
let networkProxyCtr: NetworkProxyCtr;
// 动态导入 undici Mock
// Dynamically import undici Mock
let mockUndici: any;
beforeEach(async () => {
vi.clearAllMocks();
ipcMainHandleMock.mockClear();
// 动态导入 undici Mock
// Dynamically import undici Mock
mockUndici = await import('undici');
networkProxyCtr = new NetworkProxyCtr(mockApp);
// 设置 undici mocks 的默认返回值
// Set default return values for undici mocks
vi.mocked(mockUndici.Agent).mockReturnValue({});
vi.mocked(mockUndici.ProxyAgent).mockReturnValue({});
vi.mocked(mockUndici.getGlobalDispatcher).mockReturnValue({
@@ -73,7 +73,7 @@ describe('NetworkProxyCtr', () => {
});
vi.mocked(mockUndici.setGlobalDispatcher).mockReturnValue(undefined);
// 设置 fetch mock 的默认返回值
// Set default return value for fetch mock
vi.mocked(mockUndici.fetch).mockResolvedValue({
ok: true,
status: 200,
@@ -92,7 +92,7 @@ describe('NetworkProxyCtr', () => {
};
it('should validate enabled proxy config with all required fields', () => {
// 通过测试公共方法来间接测试验证逻辑
// Indirectly test validation logic by testing public methods
expect(() => networkProxyCtr.setProxySettings(validConfig)).not.toThrow();
});
@@ -350,7 +350,7 @@ describe('NetworkProxyCtr', () => {
const invalidConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: '', // 无效的服务器
proxyServer: '', // invalid server
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
@@ -368,7 +368,7 @@ describe('NetworkProxyCtr', () => {
throw new Error('Store error');
});
// 不应该抛出错误
// Should not throw an error
await expect(networkProxyCtr.beforeAppReady()).resolves.not.toThrow();
mockStoreManager.get.mockReset();
@@ -386,7 +386,7 @@ describe('NetworkProxyCtr', () => {
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
// Indirectly test URL building by testing proxy settings
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
@@ -402,7 +402,7 @@ describe('NetworkProxyCtr', () => {
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
// Indirectly test URL building by testing proxy settings
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
@@ -418,7 +418,7 @@ describe('NetworkProxyCtr', () => {
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
// Indirectly test URL building by testing proxy settings
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
});
@@ -14,13 +14,13 @@ vi.mock('electron', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
toggleMainWindow: 'CommandOrControl+Shift+L',
openSettings: 'CommandOrControl+,',
});
const mockUpdateShortcutConfig = vi.fn().mockImplementation((id, accelerator) => {
// 简单模拟更新成功
// Simply mock a successful update
return true;
});
@@ -64,7 +64,7 @@ describe('ShortcutController', () => {
});
it('should return the result from shortcutManager.updateShortcutConfig', () => {
// 模拟更新失败的情况
// Mock an update failure scenario
mockUpdateShortcutConfig.mockReturnValueOnce(false);
const result = shortcutController.updateShortcutConfig({
@@ -19,7 +19,7 @@ vi.mock('electron', () => ({
},
}));
// 模拟 logger
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
@@ -27,10 +27,10 @@ vi.mock('@/utils/logger', () => ({
}),
}));
// 保存原始平台,确保测试结束后能恢复
// Save the original platform to restore after all tests complete
const originalPlatform = process.platform;
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockToggleVisible = vi.fn();
const mockGetMainWindow = vi.fn(() => ({
toggleVisible: mockToggleVisible,
@@ -56,14 +56,14 @@ describe('TrayMenuCtr', () => {
beforeEach(() => {
vi.clearAllMocks();
ipcMainHandleMock.mockClear();
// 为每个测试重置 mockedTray
// Reset mockedTray for each test
mockGetMainTray.mockReset();
trayMenuCtr = new TrayMenuCtr(mockApp);
});
// 在所有测试完成后恢复平台设置
// Restore platform settings after all tests complete
afterAll(() => {
// 恢复原始平台
// Restore the original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
@@ -78,7 +78,7 @@ describe('TrayMenuCtr', () => {
describe('showNotification', () => {
it('should display balloon notification on Windows platform', async () => {
// 模拟 Windows 平台
// Mock Windows platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const mockedTray = {
@@ -104,7 +104,7 @@ describe('TrayMenuCtr', () => {
});
it('should return error when not on Windows platform', async () => {
// 模拟非 Windows 平台
// Mock non-Windows platform
Object.defineProperty(process, 'platform', { value: 'darwin' });
const options: ShowTrayNotificationParams = {
@@ -123,7 +123,7 @@ describe('TrayMenuCtr', () => {
});
it('should return error when tray is not available on Windows', async () => {
// 模拟 Windows 平台但没有托盘
// Mock Windows platform with no tray
Object.defineProperty(process, 'platform', { value: 'win32' });
mockGetMainTray.mockReturnValue(null);
@@ -145,7 +145,7 @@ describe('TrayMenuCtr', () => {
describe('updateTrayIcon', () => {
it('should update tray icon on Windows platform', async () => {
// 模拟 Windows 平台
// Mock Windows platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const mockedTray = {
@@ -165,7 +165,7 @@ describe('TrayMenuCtr', () => {
});
it('should handle errors when updating icon', async () => {
// 模拟 Windows 平台
// Mock Windows platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const error = new Error('Failed to update icon');
@@ -189,7 +189,7 @@ describe('TrayMenuCtr', () => {
});
it('should return error when not on Windows platform', async () => {
// 模拟非 Windows 平台
// Mock non-Windows platform
Object.defineProperty(process, 'platform', { value: 'darwin' });
const options: UpdateTrayIconParams = {
@@ -207,7 +207,7 @@ describe('TrayMenuCtr', () => {
describe('updateTrayTooltip', () => {
it('should update tray tooltip on Windows platform', async () => {
// 模拟 Windows 平台
// Mock Windows platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const mockedTray = {
@@ -227,7 +227,7 @@ describe('TrayMenuCtr', () => {
});
it('should return error when not on Windows platform', async () => {
// 模拟非 Windows 平台
// Mock non-Windows platform
Object.defineProperty(process, 'platform', { value: 'darwin' });
const options: UpdateTrayTooltipParams = {
@@ -243,7 +243,7 @@ describe('TrayMenuCtr', () => {
});
it('should return error when tooltip is not provided', async () => {
// 模拟 Windows 平台
// Mock Windows platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const mockedTray = {
@@ -4,7 +4,7 @@ import type { App } from '@/core/App';
import UpdaterCtr from '../UpdaterCtr';
// 模拟 logger
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
info: vi.fn(),
@@ -26,7 +26,7 @@ vi.mock('electron', () => ({
},
}));
// 模拟 App 及其依赖项
// Mock App and its dependencies
const mockCheckForUpdates = vi.fn();
const mockDownloadUpdate = vi.fn();
const mockInstallNow = vi.fn();
@@ -120,13 +120,13 @@ describe('UpdaterCtr', () => {
});
});
// 测试错误处理
// Test error handling
describe('error handling', () => {
it('should handle errors when checking for updates', async () => {
const error = new Error('Network error');
mockCheckForUpdates.mockRejectedValueOnce(error);
// 由于控制器并未明确处理并返回错误,这里我们只验证调用发生且错误正确冒泡
// Since the controller does not explicitly handle and return errors, we only verify that the call occurs and the error propagates correctly
await expect(updaterCtr.checkForUpdates()).rejects.toThrow(error);
});
+3 -2
View File
@@ -1,6 +1,7 @@
import type { DesktopHotkeyId } from '@lobechat/types';
import type { App } from '@/core/App';
import { IoCContainer } from '@/core/infrastructure/IoCContainer';
import type { ShortcutActionType } from '@/shortcuts';
import { IpcService } from '@/utils/ipc';
const shortcutDecorator = (name: string) => (target: any, methodName: string, descriptor?: any) => {
@@ -15,7 +16,7 @@ const shortcutDecorator = (name: string) => (target: any, methodName: string, de
/**
* shortcut inject decorator
*/
export const shortcut = (method: ShortcutActionType) => shortcutDecorator(method);
export const shortcut = (method: DesktopHotkeyId) => shortcutDecorator(method);
const protocolDecorator =
(urlType: string, action: string) => (target: any, methodName: string, descriptor?: any) => {
@@ -5,6 +5,8 @@ import BrowserWindowsCtr from './BrowserWindowsCtr';
import CliCtr from './CliCtr';
import DevtoolsCtr from './DevtoolsCtr';
import GatewayConnectionCtr from './GatewayConnectionCtr';
import GitCtr from './GitCtr';
import HeterogeneousAgentCtr from './HeterogeneousAgentCtr';
import LocalFileCtr from './LocalFileCtr';
import McpCtr from './McpCtr';
import McpInstallCtr from './McpInstallCtr';
@@ -22,11 +24,13 @@ import UpdaterCtr from './UpdaterCtr';
import UploadFileCtr from './UploadFileCtr';
export const controllerIpcConstructors = [
HeterogeneousAgentCtr,
AuthCtr,
BrowserWindowsCtr,
CliCtr,
DevtoolsCtr,
GatewayConnectionCtr,
GitCtr,
LocalFileCtr,
McpCtr,
McpInstallCtr,
+2
View File
@@ -17,6 +17,7 @@ import { generateCliWrapper, getCliWrapperDir } from '@/modules/cliEmbedding';
import {
astSearchDetectors,
browserAutomationDetectors,
cliAgentDetectors,
contentSearchDetectors,
fileSearchDetectors,
type IToolDetector,
@@ -190,6 +191,7 @@ export class App {
const detectorCategories: Partial<Record<ToolCategory, IToolDetector[]>> = {
'runtime-environment': runtimeEnvironmentDetectors,
'cli-agents': cliAgentDetectors,
'ast-search': astSearchDetectors,
'browser-automation': browserAutomationDetectors,
'content-search': contentSearchDetectors,
@@ -4,7 +4,7 @@ import { join } from 'node:path';
import { APP_WINDOW_MIN_SIZE } from '@lobechat/desktop-bridge';
import type { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
import type { BrowserWindowConstructorOptions } from 'electron';
import { BrowserWindow, ipcMain, screen, session as electronSession, shell } from 'electron';
import { app, BrowserWindow, ipcMain, screen, session as electronSession, shell } from 'electron';
import { preloadDir, resourcesDir } from '@/const/dir';
import { isMac } from '@/const/env';
@@ -259,6 +259,13 @@ export default class Browser {
browserWindow.on('focus', () => {
logger.debug(`[${this.identifier}] Window 'focus' event fired.`);
this.broadcast('windowFocused');
// Clear any completion badge once the user returns to the app.
try {
app.setBadgeCount(0);
if (process.platform === 'darwin' && app.dock) app.dock.setBadge('');
} catch {
/* noop — some platforms may not support badge counts */
}
});
}
@@ -1,4 +1,8 @@
import type { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
import type {
MainBroadcastEventKey,
MainBroadcastParams,
TopicPopupInfo,
} from '@lobechat/electron-client-ipc';
import type { WebContents } from 'electron';
import { isLinux } from '@/const/env';
@@ -11,6 +15,9 @@ import type { App } from '../App';
import type { BrowserWindowOpts } from './Browser';
import Browser from './Browser';
const TOPIC_POPUP_TEMPLATE_ID: WindowTemplateIdentifiers = 'topicPopup';
const TOPIC_POPUP_PATH_RE = /^\/popup\/(agent|group)\/([^/?#]+)\/([^/?#]+)/;
// Create logger
const logger = createLogger('core:BrowserManager');
@@ -145,12 +152,62 @@ export class BrowserManager {
const browser = this.retrieveOrInitialize(browserOpts);
if (templateId === TOPIC_POPUP_TEMPLATE_ID) {
// Notify main-window SPAs so they can redirect to the popup instead of
// rendering the same conversation in two places. Re-emit on close to
// release the "topic is in popup" guard.
this.emitTopicPopupsChanged();
browser.browserWindow.once('closed', () => {
this.emitTopicPopupsChanged();
});
}
return {
browser,
identifier: windowId,
};
}
/**
* List currently-open topic popup windows (alive only). Used by the main
* SPA to decide whether to render the conversation or a redirect-to-popup
* guard.
*/
listTopicPopups(): TopicPopupInfo[] {
const popups: TopicPopupInfo[] = [];
this.browsers.forEach((browser, identifier) => {
if (!identifier.startsWith(`${TOPIC_POPUP_TEMPLATE_ID}_`)) return;
const webContents = browser.webContents;
if (!webContents || webContents.isDestroyed()) return;
const match = browser.options.path.match(TOPIC_POPUP_PATH_RE);
if (!match) return;
const scope = match[1] as 'agent' | 'group';
const id = match[2];
const topicId = match[3];
popups.push({
identifier,
scope,
topicId,
...(scope === 'agent' ? { agentId: id } : { groupId: id }),
});
});
return popups;
}
focusTopicPopup(identifier: string): boolean {
const browser = this.browsers.get(identifier);
if (!browser) return false;
const win = browser.browserWindow;
if (win.isMinimized()) win.restore();
win.show();
win.focus();
return true;
}
private emitTopicPopupsChanged(): void {
this.broadcastToAllWindows('topicPopupsChanged', { popups: this.listTopicPopups() });
}
/**
* Get all windows based on template
* @param templateId Template identifier
@@ -278,6 +335,16 @@ export class BrowserManager {
browser?.setWindowMinimumSize(size);
}
setWindowAlwaysOnTop(identifier: string, flag: boolean) {
const browser = this.browsers.get(identifier);
browser?.browserWindow.setAlwaysOnTop(flag);
}
isWindowAlwaysOnTop(identifier: string) {
const browser = this.browsers.get(identifier);
return browser?.browserWindow.isAlwaysOnTop() ?? false;
}
getIdentifierByWebContents(webContents: WebContents): string | null {
return this.webContentsMap.get(webContents) || null;
}
@@ -4,76 +4,89 @@ import { type App as AppCore } from '../../App';
import Browser, { type BrowserWindowOpts } from '../Browser';
// Use vi.hoisted to define mocks before hoisting
const { mockBrowserWindow, mockNativeTheme, mockIpcMain, mockScreen, MockBrowserWindow } =
vi.hoisted(() => {
const mockBrowserWindow = {
center: vi.fn(),
close: vi.fn(),
focus: vi.fn(),
getBounds: vi.fn().mockReturnValue({ height: 600, width: 800, x: 0, y: 0 }),
getContentBounds: vi.fn().mockReturnValue({ height: 600, width: 800 }),
hide: vi.fn(),
isDestroyed: vi.fn().mockReturnValue(false),
isFocused: vi.fn().mockReturnValue(true),
isFullScreen: vi.fn().mockReturnValue(false),
isMaximized: vi.fn().mockReturnValue(false),
isVisible: vi.fn().mockReturnValue(true),
loadFile: vi.fn().mockResolvedValue(undefined),
loadURL: vi.fn().mockResolvedValue(undefined),
maximize: vi.fn(),
minimize: vi.fn(),
on: vi.fn(),
once: vi.fn(),
setBackgroundColor: vi.fn(),
setBounds: vi.fn(),
setFullScreen: vi.fn(),
setPosition: vi.fn(),
setTitleBarOverlay: vi.fn(),
show: vi.fn(),
unmaximize: vi.fn(),
webContents: {
openDevTools: vi.fn(),
send: vi.fn(),
session: {
webRequest: {
onBeforeSendHeaders: vi.fn(),
onHeadersReceived: vi.fn(),
},
const {
mockElectronApp,
mockBrowserWindow,
mockNativeTheme,
mockIpcMain,
mockScreen,
MockBrowserWindow,
} = vi.hoisted(() => {
const mockBrowserWindow = {
center: vi.fn(),
close: vi.fn(),
focus: vi.fn(),
getBounds: vi.fn().mockReturnValue({ height: 600, width: 800, x: 0, y: 0 }),
getContentBounds: vi.fn().mockReturnValue({ height: 600, width: 800 }),
hide: vi.fn(),
isDestroyed: vi.fn().mockReturnValue(false),
isFocused: vi.fn().mockReturnValue(true),
isFullScreen: vi.fn().mockReturnValue(false),
isMaximized: vi.fn().mockReturnValue(false),
isVisible: vi.fn().mockReturnValue(true),
loadFile: vi.fn().mockResolvedValue(undefined),
loadURL: vi.fn().mockResolvedValue(undefined),
maximize: vi.fn(),
minimize: vi.fn(),
on: vi.fn(),
once: vi.fn(),
setBackgroundColor: vi.fn(),
setBounds: vi.fn(),
setFullScreen: vi.fn(),
setPosition: vi.fn(),
setTitleBarOverlay: vi.fn(),
show: vi.fn(),
unmaximize: vi.fn(),
webContents: {
openDevTools: vi.fn(),
send: vi.fn(),
session: {
webRequest: {
onBeforeSendHeaders: vi.fn(),
onHeadersReceived: vi.fn(),
},
on: vi.fn(),
setWindowOpenHandler: vi.fn(),
},
};
on: vi.fn(),
setWindowOpenHandler: vi.fn(),
},
};
return {
MockBrowserWindow: vi.fn().mockImplementation(() => mockBrowserWindow),
mockBrowserWindow,
mockIpcMain: {
handle: vi.fn(),
removeHandler: vi.fn(),
},
mockNativeTheme: {
off: vi.fn(),
on: vi.fn(),
shouldUseDarkColors: false,
themeSource: 'system',
},
mockScreen: {
getDisplayMatching: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
getDisplayNearestPoint: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
getPrimaryDisplay: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
},
};
});
const mockElectronApp = {
dock: { setBadge: vi.fn() },
setBadgeCount: vi.fn(),
};
return {
MockBrowserWindow: vi.fn().mockImplementation(() => mockBrowserWindow),
mockElectronApp,
mockBrowserWindow,
mockIpcMain: {
handle: vi.fn(),
removeHandler: vi.fn(),
},
mockNativeTheme: {
off: vi.fn(),
on: vi.fn(),
shouldUseDarkColors: false,
themeSource: 'system',
},
mockScreen: {
getDisplayMatching: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
getDisplayNearestPoint: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
getPrimaryDisplay: vi.fn().mockReturnValue({
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
}),
},
};
});
// Mock electron
vi.mock('electron', () => ({
app: mockElectronApp,
BrowserWindow: MockBrowserWindow,
ipcMain: mockIpcMain,
nativeTheme: mockNativeTheme,
@@ -12,8 +12,9 @@ import { RendererProtocolManager } from './RendererProtocolManager';
const logger = createLogger('core:RendererUrlManager');
// Vite build with root=monorepo preserves input path structure,
// so index.html ends up at apps/desktop/index.html in outDir.
// so index.html / popup.html end up under apps/desktop/ in outDir.
const SPA_ENTRY_HTML = join(rendererDir, 'apps', 'desktop', 'index.html');
const POPUP_ENTRY_HTML = join(rendererDir, 'apps', 'desktop', 'popup.html');
export class RendererUrlManager {
private readonly rendererProtocolManager: RendererProtocolManager;
@@ -66,7 +67,8 @@ export class RendererUrlManager {
/**
* Resolve renderer file path in production.
* Static assets map directly; all routes fall back to index.html (SPA).
* Static assets map directly; popup routes go to popup.html, all other
* routes fall back to index.html (SPA).
*/
resolveRendererFilePath = async (url: URL): Promise<string | null> => {
const pathname = url.pathname;
@@ -77,7 +79,12 @@ export class RendererUrlManager {
return pathExistsSync(filePath) ? filePath : null;
}
// All routes fallback to index.html (SPA)
// Topic popup window has its own SPA bundle.
if (pathname === '/popup' || pathname.startsWith('/popup/')) {
return POPUP_ENTRY_HTML;
}
// All other routes fallback to index.html (SPA)
return SPA_ENTRY_HTML;
};
@@ -41,6 +41,7 @@ export type ToolCategory =
| 'file-search'
| 'browser-automation'
| 'runtime-environment'
| 'cli-agents'
| 'system'
| 'custom';
@@ -1,6 +1,6 @@
import { DEFAULT_ELECTRON_DESKTOP_SHORTCUTS } from '@lobechat/const/desktopGlobalShortcuts';
import { globalShortcut } from 'electron';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { createLogger } from '@/utils/logger';
import type { App } from '../App';
@@ -77,8 +77,8 @@ export class ShortcutManager {
try {
logger.debug(`Updating shortcut ${id} to ${accelerator}`);
// 1. Check if ID is valid
if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
// 1. Check if ID is valid (value may be empty string when disabled by default)
if (!(id in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS)) {
logger.error(`Invalid shortcut ID: ${id}`);
return { errorType: 'INVALID_ID', success: false };
}
@@ -231,15 +231,15 @@ export class ShortcutManager {
// If no configuration, use default configuration
if (!config || Object.keys(config).length === 0) {
logger.debug('No shortcuts config found, using defaults');
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
this.shortcutsConfig = { ...DEFAULT_ELECTRON_DESKTOP_SHORTCUTS };
this.saveShortcutsConfig();
} else {
// Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
// Filter out invalid shortcuts that are not in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS
const filteredConfig: Record<string, string> = {};
let hasInvalidKeys = false;
Object.entries(config).forEach(([id, accelerator]) => {
if (DEFAULT_SHORTCUTS_CONFIG[id]) {
if (id in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS) {
filteredConfig[id] = accelerator;
} else {
hasInvalidKeys = true;
@@ -248,7 +248,7 @@ export class ShortcutManager {
});
// Ensure all default shortcuts are present
Object.entries(DEFAULT_SHORTCUTS_CONFIG).forEach(([id, defaultAccelerator]) => {
Object.entries(DEFAULT_ELECTRON_DESKTOP_SHORTCUTS).forEach(([id, defaultAccelerator]) => {
if (!(id in filteredConfig)) {
filteredConfig[id] = defaultAccelerator;
logger.debug(`Adding missing default shortcut: ${id} = ${defaultAccelerator}`);
@@ -267,7 +267,7 @@ export class ShortcutManager {
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
} catch (error) {
logger.error('Error loading shortcuts config:', error);
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
this.shortcutsConfig = { ...DEFAULT_ELECTRON_DESKTOP_SHORTCUTS };
this.saveShortcutsConfig();
}
}
@@ -295,9 +295,9 @@ export class ShortcutManager {
Object.entries(this.shortcutsConfig).forEach(([id, accelerator]) => {
logger.debug(`Registering shortcut '${id}' with ${accelerator}`);
// Only register shortcuts that exist in DEFAULT_SHORTCUTS_CONFIG
if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
logger.debug(`Skipping shortcut '${id}' - not found in DEFAULT_SHORTCUTS_CONFIG`);
// Only register shortcuts that exist in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS
if (!(id in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS)) {
logger.debug(`Skipping shortcut '${id}' - not found in DEFAULT_ELECTRON_DESKTOP_SHORTCUTS`);
return;
}
@@ -1,8 +1,7 @@
import { DEFAULT_ELECTRON_DESKTOP_SHORTCUTS } from '@lobechat/const/desktopGlobalShortcuts';
import { globalShortcut } from 'electron';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import type { App } from '../../App';
import { ShortcutManager } from '../ShortcutManager';
@@ -26,10 +25,10 @@ vi.mock('@/utils/logger', () => ({
}),
}));
// Mock DEFAULT_SHORTCUTS_CONFIG
vi.mock('@/shortcuts', () => ({
DEFAULT_SHORTCUTS_CONFIG: {
showApp: 'Control+E',
// Mock desktop global shortcut defaults
vi.mock('@lobechat/const/desktopGlobalShortcuts', () => ({
DEFAULT_ELECTRON_DESKTOP_SHORTCUTS: {
showApp: '',
openSettings: 'CommandOrControl+,',
},
}));
@@ -115,7 +114,7 @@ describe('ShortcutManager', () => {
expect(mockStoreManager.get).toHaveBeenCalledWith('shortcuts');
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
expect(globalShortcut.register).toHaveBeenCalledWith('Control+E', expect.any(Function));
expect(globalShortcut.register).not.toHaveBeenCalledWith('Control+E', expect.any(Function));
expect(globalShortcut.register).toHaveBeenCalledWith(
'CommandOrControl+,',
expect.any(Function),
@@ -145,7 +144,7 @@ describe('ShortcutManager', () => {
shortcutManager.initialize();
const config = shortcutManager.getShortcutsConfig();
expect(config).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(config).toEqual(DEFAULT_ELECTRON_DESKTOP_SHORTCUTS);
});
});
@@ -346,8 +345,11 @@ describe('ShortcutManager', () => {
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_ELECTRON_DESKTOP_SHORTCUTS);
expect(mockStoreManager.set).toHaveBeenCalledWith(
'shortcuts',
DEFAULT_ELECTRON_DESKTOP_SHORTCUTS,
);
});
it('should use defaults when config is empty', () => {
@@ -355,7 +357,7 @@ describe('ShortcutManager', () => {
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_ELECTRON_DESKTOP_SHORTCUTS);
});
it('should filter invalid keys from stored config', () => {
@@ -413,8 +415,11 @@ describe('ShortcutManager', () => {
shortcutManager['loadShortcutsConfig']();
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_ELECTRON_DESKTOP_SHORTCUTS);
expect(mockStoreManager.set).toHaveBeenCalledWith(
'shortcuts',
DEFAULT_ELECTRON_DESKTOP_SHORTCUTS,
);
});
});
@@ -458,7 +463,7 @@ describe('ShortcutManager', () => {
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
});
it('should skip shortcuts not in DEFAULT_SHORTCUTS_CONFIG', () => {
it('should skip shortcuts not defined in default electron desktop shortcuts', () => {
shortcutManager['shortcutsConfig'] = {
showApp: 'Alt+E',
invalidKey: 'Ctrl+I',
+435
View File
@@ -0,0 +1,435 @@
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import type { Readable } from 'node:stream';
import { createLogger } from '@/utils/logger';
import type {
ACPInitializeParams,
ACPPermissionRequest,
ACPPermissionResponse,
ACPServerCapabilities,
ACPSessionCancelParams,
ACPSessionInfo,
ACPSessionNewParams,
ACPSessionPromptParams,
ACPSessionUpdate,
FSReadTextFileParams,
FSReadTextFileResult,
FSWriteTextFileParams,
JsonRpcError,
JsonRpcNotification,
JsonRpcRequest,
JsonRpcResponse,
TerminalCreateParams,
TerminalCreateResult,
TerminalKillParams,
TerminalOutputParams,
TerminalOutputResult,
TerminalReleaseParams,
TerminalWaitForExitParams,
TerminalWaitForExitResult,
} from './types';
const logger = createLogger('libs:acp:client');
type PendingRequest = {
reject: (error: Error) => void;
resolve: (result: unknown) => void;
};
export interface ACPClientParams {
args?: string[];
command: string;
cwd?: string;
env?: Record<string, string>;
}
export interface ACPClientCallbacks {
onPermissionRequest?: (request: ACPPermissionRequest) => Promise<ACPPermissionResponse>;
onSessionComplete?: (sessionId: string) => void;
onSessionUpdate?: (update: ACPSessionUpdate) => void;
}
/**
* ACP Client that communicates with an ACP agent (e.g. Claude Code) over stdio JSON-RPC 2.0.
*
* Bidirectional: sends requests to agent AND handles incoming requests from agent
* (fs/read_text_file, fs/write_text_file, terminal/*, session/request_permission).
*/
export class ACPClient {
private buffer = '';
private callbacks: ACPClientCallbacks = {};
private nextId = 1;
private pendingRequests = new Map<number | string, PendingRequest>();
private process: ChildProcess | null = null;
private stderrLogs: string[] = [];
// Client-side method handlers (agent calls these)
private clientMethodHandlers = new Map<string, (params: any) => Promise<unknown>>();
constructor(private readonly params: ACPClientParams) {}
/**
* Register handlers for client-side methods that the agent can call back.
*/
registerClientMethods(handlers: {
'fs/read_text_file'?: (params: FSReadTextFileParams) => Promise<FSReadTextFileResult>;
'fs/write_text_file'?: (params: FSWriteTextFileParams) => Promise<void>;
'terminal/create'?: (params: TerminalCreateParams) => Promise<TerminalCreateResult>;
'terminal/kill'?: (params: TerminalKillParams) => Promise<void>;
'terminal/output'?: (params: TerminalOutputParams) => Promise<TerminalOutputResult>;
'terminal/release'?: (params: TerminalReleaseParams) => Promise<void>;
'terminal/wait_for_exit'?: (
params: TerminalWaitForExitParams,
) => Promise<TerminalWaitForExitResult>;
}) {
for (const [method, handler] of Object.entries(handlers)) {
if (handler) {
this.clientMethodHandlers.set(method, handler);
}
}
}
setCallbacks(callbacks: ACPClientCallbacks) {
this.callbacks = callbacks;
}
/**
* Spawn the agent process and initialize the ACP connection.
*/
async connect(): Promise<ACPServerCapabilities> {
const { command, args = [], env, cwd } = this.params;
this.process = spawn(command, args, {
cwd,
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe'],
});
// Capture stderr
const stderr = this.process.stderr as Readable | null;
if (stderr) {
stderr.on('data', (chunk: Buffer) => {
const lines = chunk
.toString('utf8')
.split('\n')
.filter((l) => l.trim());
this.stderrLogs.push(...lines);
});
}
// Listen for stdout (JSON-RPC messages)
const stdout = this.process.stdout as Readable | null;
if (stdout) {
stdout.on('data', (chunk: Buffer) => {
this.handleData(chunk.toString('utf8'));
});
}
this.process.on('error', (err) => {
logger.error('ACP process error:', err);
});
this.process.on('exit', (code, signal) => {
logger.info('ACP process exited:', { code, signal });
// Reject all pending requests
for (const [id, pending] of this.pendingRequests) {
pending.reject(new Error(`ACP process exited (code=${code}, signal=${signal})`));
this.pendingRequests.delete(id);
}
});
// Initialize
const capabilities = await this.initialize();
return capabilities;
}
/**
* Send initialize request to the agent.
*/
private async initialize(): Promise<ACPServerCapabilities> {
const params: ACPInitializeParams = {
capabilities: {
fs: { readTextFile: true, writeTextFile: true },
terminal: true,
},
clientInfo: { name: 'lobehub-desktop', version: '1.0.0' },
protocolVersion: '0.1',
};
return this.sendRequest<ACPServerCapabilities>('initialize', params);
}
/**
* Create a new session.
*/
async createSession(params?: ACPSessionNewParams): Promise<ACPSessionInfo> {
return this.sendRequest<ACPSessionInfo>('session/new', params);
}
/**
* Send a prompt to an existing session.
*/
async sendPrompt(params: ACPSessionPromptParams): Promise<void> {
return this.sendRequest<void>('session/prompt', params);
}
/**
* Cancel an ongoing session operation.
*/
async cancelSession(params: ACPSessionCancelParams): Promise<void> {
return this.sendRequest<void>('session/cancel', params);
}
/**
* Respond to a permission request from the agent.
*/
respondToPermission(requestId: string, response: ACPPermissionResponse): void {
this.sendResponse(requestId, response);
}
/**
* Disconnect from the agent and kill the process.
*/
async disconnect(): Promise<void> {
if (this.process) {
this.process.stdin?.end();
this.process.kill('SIGTERM');
// Force kill after timeout
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (this.process && !this.process.killed) {
this.process.kill('SIGKILL');
}
resolve();
}, 5000);
this.process?.on('exit', () => {
clearTimeout(timeout);
resolve();
});
});
this.process = null;
}
}
getStderrLogs(): string[] {
return this.stderrLogs;
}
// ============================================================
// JSON-RPC transport layer
// ============================================================
private sendRequest<T>(method: string, params?: object): Promise<T> {
return new Promise((resolve, reject) => {
const id = this.nextId++;
const request: JsonRpcRequest = {
id,
jsonrpc: '2.0',
method,
params,
};
this.pendingRequests.set(id, {
reject,
resolve: resolve as (result: unknown) => void,
});
this.writeMessage(request);
});
}
private sendResponse(id: number | string, result: unknown): void {
const response: JsonRpcResponse = {
id,
jsonrpc: '2.0',
result,
};
this.writeMessage(response);
}
private sendErrorResponse(id: number | string, error: JsonRpcError): void {
const response: JsonRpcResponse = {
error,
id,
jsonrpc: '2.0',
};
this.writeMessage(response);
}
private writeMessage(message: JsonRpcRequest | JsonRpcResponse | JsonRpcNotification): void {
if (!this.process?.stdin?.writable) {
logger.error('Cannot write to ACP process: stdin not writable');
return;
}
const json = JSON.stringify(message);
const content = `Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`;
this.process.stdin.write(content);
}
/**
* Handle incoming data from stdout, parsing JSON-RPC messages.
* Uses Content-Length header framing (LSP-style).
*/
private handleData(data: string): void {
this.buffer += data;
while (true) {
// Try to parse a complete message from the buffer
const headerEnd = this.buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) break;
const header = this.buffer.slice(0, headerEnd);
const contentLengthMatch = header.match(/Content-Length:\s*(\d+)/i);
if (!contentLengthMatch) {
// Try parsing as raw JSON (some agents don't use Content-Length headers)
const newlineIdx = this.buffer.indexOf('\n');
if (newlineIdx === -1) break;
const line = this.buffer.slice(0, newlineIdx).trim();
this.buffer = this.buffer.slice(newlineIdx + 1);
if (line) {
try {
const message = JSON.parse(line);
this.handleMessage(message);
} catch {
// Not valid JSON, skip
}
}
continue;
}
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
const messageStart = headerEnd + 4; // after \r\n\r\n
const messageEnd = messageStart + contentLength;
if (Buffer.byteLength(this.buffer.slice(messageStart)) < contentLength) {
// Not enough data yet
break;
}
const messageStr = this.buffer.slice(messageStart, messageEnd);
this.buffer = this.buffer.slice(messageEnd);
try {
const message = JSON.parse(messageStr);
this.handleMessage(message);
} catch (err) {
logger.error('Failed to parse ACP JSON-RPC message:', err);
}
}
}
/**
* Route incoming JSON-RPC messages.
*/
private handleMessage(message: JsonRpcRequest | JsonRpcResponse | JsonRpcNotification): void {
// Response to our request
if ('id' in message && message.id !== null && !('method' in message)) {
const response = message as JsonRpcResponse;
const pending = this.pendingRequests.get(response.id!);
if (pending) {
this.pendingRequests.delete(response.id!);
if (response.error) {
pending.reject(
new Error(`ACP error [${response.error.code}]: ${response.error.message}`),
);
} else {
pending.resolve(response.result);
}
}
return;
}
// Incoming request or notification from agent
if ('method' in message) {
const method = message.method;
const params = message.params || {};
// Notification (no id) — e.g., session/update
if (!('id' in message) || message.id === undefined || message.id === null) {
this.handleNotification(method, params);
return;
}
// Request (has id) — agent calling client methods
this.handleIncomingRequest(message as JsonRpcRequest);
}
}
/**
* Handle notifications from the agent (no response expected).
*/
private handleNotification(method: string, params: Record<string, unknown> | object): void {
switch (method) {
case 'session/update': {
if (this.callbacks.onSessionUpdate) {
this.callbacks.onSessionUpdate(params as unknown as ACPSessionUpdate);
}
break;
}
default: {
logger.warn('Unhandled ACP notification:', method);
}
}
}
/**
* Handle incoming requests from the agent (response required).
*/
private async handleIncomingRequest(request: JsonRpcRequest): Promise<void> {
const { id, method, params } = request;
// Special handling for permission requests
if (method === 'session/request_permission') {
if (this.callbacks.onPermissionRequest) {
try {
const response = await this.callbacks.onPermissionRequest(
params as unknown as ACPPermissionRequest,
);
this.sendResponse(id, response);
} catch (err) {
this.sendErrorResponse(id, {
code: -32000,
message: err instanceof Error ? err.message : 'Permission request failed',
});
}
} else {
// Auto-allow if no handler
const permReq = params as unknown as ACPPermissionRequest;
const allowOption = permReq.options?.find((o) => o.kind === 'allow_once');
this.sendResponse(id, {
kind: 'selected',
optionId: allowOption?.optionId || permReq.options?.[0]?.optionId,
});
}
return;
}
// Delegate to registered client method handlers
const handler = this.clientMethodHandlers.get(method);
if (handler) {
try {
const result = await handler(params);
this.sendResponse(id, result ?? null);
} catch (err) {
this.sendErrorResponse(id, {
code: -32000,
message: err instanceof Error ? err.message : 'Client method failed',
});
}
} else {
this.sendErrorResponse(id, {
code: -32601,
message: `Method not found: ${method}`,
});
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export type { ACPClientCallbacks, ACPClientParams } from './client';
export { ACPClient } from './client';
export type * from './types';
+326
View File
@@ -0,0 +1,326 @@
/**
* ACP (Agent Client Protocol) type definitions
* Based on: https://agentclientprotocol.com/protocol/schema
*/
// ============================================================
// JSON-RPC 2.0 base types
// ============================================================
export interface JsonRpcRequest {
id: number | string;
jsonrpc: '2.0';
method: string;
params?: Record<string, unknown> | object;
}
export interface JsonRpcResponse {
error?: JsonRpcError;
id: number | string | null;
jsonrpc: '2.0';
result?: unknown;
}
export interface JsonRpcNotification {
jsonrpc: '2.0';
method: string;
params?: Record<string, unknown>;
}
export interface JsonRpcError {
code: number;
data?: unknown;
message: string;
}
// ============================================================
// ACP Capabilities
// ============================================================
export interface ACPCapabilities {
audio?: boolean;
embeddedContext?: boolean;
fs?: {
readTextFile?: boolean;
writeTextFile?: boolean;
};
image?: boolean;
terminal?: boolean;
}
export interface ACPServerCapabilities {
modes?: ACPMode[];
name: string;
protocolVersion: string;
version?: string;
}
export interface ACPMode {
description?: string;
id: string;
name: string;
}
// ============================================================
// Session types
// ============================================================
export interface ACPSessionInfo {
createdAt?: string;
id: string;
title?: string;
}
// ============================================================
// Content block types (used in session/update)
// ============================================================
export type ACPContentBlock =
| ACPTextContent
| ACPImageContent
| ACPAudioContent
| ACPResourceContent
| ACPResourceLinkContent;
export interface ACPTextContent {
annotations?: Record<string, unknown>;
text: string;
type: 'text';
}
export interface ACPImageContent {
annotations?: Record<string, unknown>;
data: string;
mimeType: string;
type: 'image';
uri?: string;
}
export interface ACPAudioContent {
annotations?: Record<string, unknown>;
data: string;
mimeType: string;
type: 'audio';
}
export interface ACPResourceContent {
annotations?: Record<string, unknown>;
resource: {
blob?: string;
mimeType?: string;
text?: string;
uri: string;
};
type: 'resource';
}
export interface ACPResourceLinkContent {
annotations?: Record<string, unknown>;
description?: string;
mimeType?: string;
name: string;
size?: number;
title?: string;
type: 'resource_link';
uri: string;
}
// ============================================================
// Tool call types
// ============================================================
export type ACPToolCallKind =
| 'read'
| 'edit'
| 'delete'
| 'move'
| 'search'
| 'execute'
| 'think'
| 'fetch'
| 'other';
export type ACPToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
export interface ACPToolCallDiffContent {
newText: string;
oldText: string;
path: string;
type: 'diff';
}
export interface ACPToolCallTerminalContent {
command?: string;
exitCode?: number;
output: string;
type: 'terminal';
}
export type ACPToolCallContent =
| ACPTextContent
| ACPImageContent
| ACPToolCallDiffContent
| ACPToolCallTerminalContent;
export interface ACPToolCallLocation {
endLine?: number;
path: string;
startLine?: number;
}
export interface ACPToolCallUpdate {
content?: ACPToolCallContent[];
kind?: ACPToolCallKind;
locations?: ACPToolCallLocation[];
rawInput?: string;
rawOutput?: string;
status?: ACPToolCallStatus;
title: string;
toolCallId: string;
}
// ============================================================
// Session update notification
// ============================================================
export type ACPMessageRole = 'assistant' | 'user' | 'thought';
export interface ACPMessageChunk {
content: ACPContentBlock[];
role: ACPMessageRole;
}
export interface ACPSessionUpdate {
messageChunks?: ACPMessageChunk[];
sessionId: string;
toolCalls?: ACPToolCallUpdate[];
}
// ============================================================
// Permission request types
// ============================================================
export interface ACPPermissionOption {
kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
name: string;
optionId: string;
}
export interface ACPPermissionRequest {
message?: string;
options: ACPPermissionOption[];
sessionId: string;
toolCall?: ACPToolCallUpdate;
}
export interface ACPPermissionResponse {
kind: 'selected' | 'cancelled';
optionId?: string;
}
// ============================================================
// Client method params (agent → client)
// ============================================================
export interface FSReadTextFileParams {
path: string;
}
export interface FSReadTextFileResult {
text: string;
}
export interface FSWriteTextFileParams {
path: string;
text: string;
}
export interface TerminalCreateParams {
command: string;
cwd?: string;
env?: Record<string, string>;
}
export interface TerminalCreateResult {
terminalId: string;
}
export interface TerminalOutputParams {
terminalId: string;
}
export interface TerminalOutputResult {
exitCode?: number;
isRunning: boolean;
output: string;
}
export interface TerminalWaitForExitParams {
terminalId: string;
timeout?: number;
}
export interface TerminalWaitForExitResult {
exitCode: number;
output: string;
}
export interface TerminalKillParams {
terminalId: string;
}
export interface TerminalReleaseParams {
terminalId: string;
}
// ============================================================
// Agent method params (client → agent)
// ============================================================
export interface ACPInitializeParams {
capabilities?: ACPCapabilities;
clientInfo?: {
name: string;
version: string;
};
protocolVersion: string;
}
export interface ACPSessionNewParams {
title?: string;
}
export interface ACPSessionPromptParams {
content: ACPContentBlock[];
sessionId: string;
}
export interface ACPSessionCancelParams {
sessionId: string;
}
// ============================================================
// Broadcast event types (main → renderer)
// ============================================================
export interface ACPSessionUpdateEvent {
sessionId: string;
update: ACPSessionUpdate;
}
export interface ACPPermissionRequestEvent {
message?: string;
options: ACPPermissionOption[];
requestId: string;
sessionId: string;
toolCall?: ACPToolCallUpdate;
}
export interface ACPSessionErrorEvent {
error: string;
sessionId: string;
}
export interface ACPSessionCompleteEvent {
sessionId: string;
}
@@ -48,6 +48,7 @@ const menu = {
'file.newAgent': 'New Agent',
'file.newAgentGroup': 'New Agent Group',
'file.newPage': 'New Page',
'file.newTab': 'New Tab',
'file.newTopic': 'New Topic',
'file.preferences': 'Preferences',
'file.quit': 'Quit',
@@ -309,14 +309,16 @@ describe('LinuxMenu', () => {
expect(copyItem.role).toBe('copy');
});
it('should use role for close (accelerator handled by Electron)', () => {
it('should bind CmdOrCtrl+W to a smart close handler (tab first, then window)', () => {
linuxMenu.buildAndSetAppMenu();
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
const fileMenu = template.find((item: any) => item.label === 'File');
const closeItem = fileMenu.submenu.find((item: any) => item.label === 'Close');
expect(closeItem.role).toBe('close');
expect(closeItem.accelerator).toBe('CmdOrCtrl+W');
expect(typeof closeItem.click).toBe('function');
expect(closeItem.role).toBeUndefined();
});
it('should use role for minimize (accelerator handled by Electron)', () => {
+24 -2
View File
@@ -1,5 +1,5 @@
import type { MenuItemConstructorOptions } from 'electron';
import { app, clipboard, dialog, Menu, shell } from 'electron';
import { app, BrowserWindow, clipboard, dialog, Menu, shell } from 'electron';
import { isDev } from '@/const/env';
@@ -64,6 +64,15 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
},
label: t('file.newTopic'),
},
{
accelerator: 'Ctrl+T',
click: () => {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('createNewTab');
},
label: t('file.newTab'),
},
{ type: 'separator' },
{
accelerator: 'Alt+Ctrl+A',
@@ -104,7 +113,20 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
label: t('common.checkUpdates'),
},
{ type: 'separator' },
{ label: t('window.close'), role: 'close' },
{
accelerator: 'CmdOrCtrl+W',
click: () => {
const focused = BrowserWindow.getFocusedWindow();
if (!focused) return;
const mainWindow = this.app.browserManager.getMainWindow();
if (focused === mainWindow.browserWindow) {
mainWindow.broadcast('closeCurrentTabOrWindow');
} else {
focused.close();
}
},
label: t('window.close'),
},
{ label: t('window.minimize'), role: 'minimize' },
{ type: 'separator' },
{ label: t('file.quit'), role: 'quit' },
+24 -2
View File
@@ -1,7 +1,7 @@
import * as path from 'node:path';
import type { MenuItemConstructorOptions } from 'electron';
import { app, clipboard, Menu, shell } from 'electron';
import { app, BrowserWindow, clipboard, Menu, shell } from 'electron';
import { isDev } from '@/const/env';
import NotificationCtr from '@/controllers/NotificationCtr';
@@ -116,6 +116,15 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
},
label: t('file.newTopic'),
},
{
accelerator: 'Command+T',
click: () => {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('createNewTab');
},
label: t('file.newTab'),
},
{ type: 'separator' },
{
accelerator: 'Alt+Command+A',
@@ -145,7 +154,20 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
label: t('file.newPage'),
},
{ type: 'separator' },
{ label: t('window.close'), role: 'close' },
{
accelerator: 'CmdOrCtrl+W',
click: () => {
const focused = BrowserWindow.getFocusedWindow();
if (!focused) return;
const mainWindow = this.app.browserManager.getMainWindow();
if (focused === mainWindow.browserWindow) {
mainWindow.broadcast('closeCurrentTabOrWindow');
} else {
focused.close();
}
},
label: t('window.close'),
},
],
},
{
@@ -398,10 +398,12 @@ describe('WindowsMenu', () => {
const windowMenu = template.find((item: any) => item.label === 'Window');
const minimizeItem = windowMenu.submenu.find((item: any) => item.role === 'minimize');
const closeItem = windowMenu.submenu.find((item: any) => item.role === 'close');
const closeItem = windowMenu.submenu.find((item: any) => item.label === 'Close');
expect(minimizeItem).toBeDefined();
expect(closeItem).toBeDefined();
expect(closeItem.accelerator).toBe('CmdOrCtrl+W');
expect(typeof closeItem.click).toBe('function');
});
it('should have zoom controls in view menu', () => {
+24 -2
View File
@@ -1,5 +1,5 @@
import type { MenuItemConstructorOptions } from 'electron';
import { app, clipboard, Menu, shell } from 'electron';
import { app, BrowserWindow, clipboard, Menu, shell } from 'electron';
import { isDev } from '@/const/env';
@@ -63,6 +63,15 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
},
label: t('file.newTopic'),
},
{
accelerator: 'Ctrl+T',
click: () => {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('createNewTab');
},
label: t('file.newTab'),
},
{ type: 'separator' },
{
accelerator: 'Alt+Ctrl+A',
@@ -167,7 +176,20 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform {
label: t('window.title'),
submenu: [
{ label: t('window.minimize'), role: 'minimize' },
{ label: t('window.close'), role: 'close' },
{
accelerator: 'CmdOrCtrl+W',
click: () => {
const focused = BrowserWindow.getFocusedWindow();
if (!focused) return;
const mainWindow = this.app.browserManager.getMainWindow();
if (focused === mainWindow.browserWindow) {
mainWindow.broadcast('closeCurrentTabOrWindow');
} else {
focused.close();
}
},
label: t('window.close'),
},
],
},
{
@@ -0,0 +1,81 @@
import type { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { describe, expect, it } from 'vitest';
import { buildProxyEnv } from '../envBuilder';
describe('buildProxyEnv', () => {
const baseConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
it('should return empty object when proxy is disabled', () => {
const env = buildProxyEnv({ ...baseConfig, enableProxy: false });
expect(env).toEqual({});
});
it('should return empty object when proxy server is empty', () => {
const env = buildProxyEnv({ ...baseConfig, proxyServer: '' });
expect(env).toEqual({});
});
it('should return empty object when proxy port is empty', () => {
const env = buildProxyEnv({ ...baseConfig, proxyPort: '' });
expect(env).toEqual({});
});
it('should set HTTP(S)_PROXY for http proxy', () => {
const env = buildProxyEnv({ ...baseConfig, proxyType: 'http' });
expect(env.HTTP_PROXY).toBe('http://proxy.example.com:8080');
expect(env.HTTPS_PROXY).toBe('http://proxy.example.com:8080');
expect(env.ALL_PROXY).toBeUndefined();
});
it('should set HTTP(S)_PROXY for https proxy', () => {
const env = buildProxyEnv({ ...baseConfig, proxyType: 'https' });
expect(env.HTTP_PROXY).toBe('https://proxy.example.com:8080');
expect(env.HTTPS_PROXY).toBe('https://proxy.example.com:8080');
expect(env.ALL_PROXY).toBeUndefined();
});
it('should set ALL_PROXY for socks5 proxy and skip HTTP(S)_PROXY', () => {
const env = buildProxyEnv({ ...baseConfig, proxyType: 'socks5' });
expect(env.ALL_PROXY).toBe('socks5://proxy.example.com:8080');
expect(env.HTTP_PROXY).toBeUndefined();
expect(env.HTTPS_PROXY).toBeUndefined();
});
it('should include NO_PROXY from proxyBypass', () => {
const env = buildProxyEnv(baseConfig);
expect(env.NO_PROXY).toBe('localhost,127.0.0.1,::1');
});
it('should omit NO_PROXY when proxyBypass is empty', () => {
const env = buildProxyEnv({ ...baseConfig, proxyBypass: '' });
expect(env.NO_PROXY).toBeUndefined();
});
it('should include auth in proxy URL', () => {
const env = buildProxyEnv({
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user',
proxyPassword: 'pass',
});
expect(env.HTTP_PROXY).toBe('http://user:pass@proxy.example.com:8080');
expect(env.HTTPS_PROXY).toBe('http://user:pass@proxy.example.com:8080');
});
});
@@ -0,0 +1,36 @@
import type { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { ProxyUrlBuilder } from './urlBuilder';
/**
* Build proxy env vars (HTTPS_PROXY / HTTP_PROXY / ALL_PROXY / NO_PROXY) to
* forward the user's proxy config to spawned child processes (e.g. CLI tools
* like claude-code, codex, MCP stdio servers). The in-process undici
* dispatcher set by ProxyDispatcherManager only covers the main process —
* children need env vars to pick it up.
*
* Returns `{}` when proxy is disabled, so callers can unconditionally spread
* the result into the spawn env.
*/
export const buildProxyEnv = (config?: NetworkProxySettings): Record<string, string> => {
if (!config?.enableProxy || !config.proxyServer || !config.proxyPort) {
return {};
}
const url = ProxyUrlBuilder.build(config);
const env: Record<string, string> = {};
// SOCKS5 is not universally supported via HTTP(S)_PROXY — stick to ALL_PROXY.
if (config.proxyType === 'socks5') {
env.ALL_PROXY = url;
} else {
env.HTTP_PROXY = url;
env.HTTPS_PROXY = url;
}
if (config.proxyBypass) {
env.NO_PROXY = config.proxyBypass;
}
return env;
};
@@ -1,4 +1,5 @@
export { ProxyDispatcherManager } from './dispatcher';
export { buildProxyEnv } from './envBuilder';
export type { ProxyTestResult } from './tester';
export { ProxyConnectionTester } from './tester';
export { ProxyUrlBuilder } from './urlBuilder';
@@ -0,0 +1,145 @@
import { exec } from 'node:child_process';
import { platform } from 'node:os';
import { promisify } from 'node:util';
import type { IToolDetector, ToolStatus } from '@/core/infrastructure/ToolDetectorManager';
import { createCommandDetector } from '@/core/infrastructure/ToolDetectorManager';
const execPromise = promisify(exec);
/**
* Detector that resolves a command path via which/where, then validates
* the binary by matching `--version` (or `--help`) output against a keyword
* to avoid collisions with unrelated executables of the same name.
*/
const createValidatedDetector = (options: {
candidates: string[];
description: string;
name: string;
priority: number;
validateFlag?: string;
validateKeywords: string[];
}): IToolDetector => {
const {
name,
description,
priority,
candidates,
validateFlag = '--version',
validateKeywords,
} = options;
return {
description,
async detect(): Promise<ToolStatus> {
const whichCmd = platform() === 'win32' ? 'where' : 'which';
for (const cmd of candidates) {
try {
const { stdout: pathOut } = await execPromise(`${whichCmd} ${cmd}`, { timeout: 3000 });
const toolPath = pathOut.trim().split('\n')[0];
if (!toolPath) continue;
const { stdout: out } = await execPromise(`${cmd} ${validateFlag}`, { timeout: 5000 });
const output = out.trim();
const lowered = output.toLowerCase();
if (!validateKeywords.some((kw) => lowered.includes(kw.toLowerCase()))) continue;
return {
available: true,
path: toolPath,
version: output.split('\n')[0],
};
} catch {
continue;
}
}
return { available: false };
},
name,
priority,
};
};
/**
* Claude Code CLI
* @see https://docs.claude.com/en/docs/claude-code
*/
export const claudeCodeDetector: IToolDetector = createValidatedDetector({
candidates: ['claude'],
description: 'Claude Code - Anthropic official agentic coding CLI',
name: 'claude',
priority: 1,
validateKeywords: ['claude code'],
});
/**
* OpenAI Codex CLI
* @see https://github.com/openai/codex
*/
export const codexDetector: IToolDetector = createValidatedDetector({
candidates: ['codex'],
description: 'Codex - OpenAI agentic coding CLI',
name: 'codex',
priority: 2,
validateKeywords: ['codex'],
});
/**
* Google Gemini CLI
* @see https://github.com/google-gemini/gemini-cli
*/
export const geminiCliDetector: IToolDetector = createValidatedDetector({
candidates: ['gemini'],
description: 'Gemini CLI - Google agentic coding CLI',
name: 'gemini',
priority: 3,
validateKeywords: ['gemini'],
});
/**
* Qwen Code CLI
* @see https://github.com/QwenLM/qwen-code
*/
export const qwenCodeDetector: IToolDetector = createValidatedDetector({
candidates: ['qwen'],
description: 'Qwen Code - Alibaba Qwen agentic coding CLI',
name: 'qwen',
priority: 4,
validateKeywords: ['qwen'],
});
/**
* Kimi CLI (Moonshot)
* @see https://github.com/MoonshotAI/kimi-cli
*/
export const kimiCliDetector: IToolDetector = createValidatedDetector({
candidates: ['kimi'],
description: 'Kimi CLI - Moonshot AI agentic coding CLI',
name: 'kimi',
priority: 5,
validateKeywords: ['kimi'],
});
/**
* Aider - AI pair programming CLI
* Generic command detector; name collision is unlikely.
* @see https://github.com/Aider-AI/aider
*/
export const aiderDetector: IToolDetector = createCommandDetector('aider', {
description: 'Aider - AI pair programming in your terminal',
priority: 6,
});
/**
* All CLI agent detectors
*/
export const cliAgentDetectors: IToolDetector[] = [
claudeCodeDetector,
codexDetector,
geminiCliDetector,
qwenCodeDetector,
kimiCliDetector,
aiderDetector,
];
@@ -6,6 +6,7 @@
*/
export { browserAutomationDetectors } from './agentBrowserDetectors';
export { cliAgentDetectors } from './cliAgentDetectors';
export { astSearchDetectors, contentSearchDetectors } from './contentSearchDetectors';
export { fileSearchDetectors } from './fileSearchDetectors';
export { runtimeEnvironmentDetectors } from './runtimeEnvironmentDetectors';
-20
View File
@@ -1,20 +0,0 @@
/**
* Shortcut action type enum
*/
export const ShortcutActionEnum = {
openSettings: 'openSettings',
/**
* Show/hide main window
*/
showApp: 'showApp',
} as const;
export type ShortcutActionType = (typeof ShortcutActionEnum)[keyof typeof ShortcutActionEnum];
/**
* Default shortcut configuration
*/
export const DEFAULT_SHORTCUTS_CONFIG: Record<ShortcutActionType, string> = {
[ShortcutActionEnum.showApp]: 'Control+E',
[ShortcutActionEnum.openSettings]: 'CommandOrControl+,',
};
-1
View File
@@ -1 +0,0 @@
export * from './config';
@@ -0,0 +1,65 @@
import { readFile } from 'node:fs/promises';
import { readdir } from 'fs-extra';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { detectRepoType } from '../git';
vi.mock('node:fs/promises', () => ({
readFile: vi.fn(),
}));
vi.mock('fs-extra', () => ({
readdir: vi.fn(),
}));
const eisdir = () => Object.assign(new Error('EISDIR'), { code: 'EISDIR' });
const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
// Stub a flat (non-worktree) git repo at /repo whose config is `config`.
const stubRepo = (config: string) => {
vi.mocked(readFile).mockImplementation(async (filePath) => {
const p = String(filePath);
if (p.endsWith('/.git')) throw eisdir(); // .git is a directory
if (p.endsWith('/commondir')) throw enoent(); // no commondir → use gitDir
if (p.endsWith('/config')) return config;
throw new Error(`unexpected readFile: ${p}`);
});
vi.mocked(readdir).mockResolvedValue(['HEAD', 'config'] as any);
};
describe('detectRepoType', () => {
afterEach(() => {
vi.clearAllMocks();
});
it.each([
['SCP-like SSH', '[remote "origin"]\n\turl = git@github.com:owner/repo.git\n'],
['HTTPS', '[remote "origin"]\n\turl = https://github.com/owner/repo.git\n'],
['ssh:// scheme', '[remote "origin"]\n\turl = ssh://git@github.com/owner/repo.git\n'],
['git:// scheme', '[remote "origin"]\n\turl = git://github.com/owner/repo.git\n'],
])('classifies %s remote as github', async (_label, config) => {
stubRepo(config);
await expect(detectRepoType('/repo')).resolves.toBe('github');
});
it.each([
['look-alike host', '[remote "origin"]\n\turl = git@evilgithub.com:owner/repo.git\n'],
['suffix injection', '[remote "origin"]\n\turl = https://github.com.attacker.com/x.git\n'],
[
'github.com inside path of unrelated host',
'[remote "origin"]\n\turl = https://attacker.com/?ref=github.com/x\n',
],
['GitHub Enterprise', '[remote "origin"]\n\turl = git@github.example.com:owner/repo.git\n'],
['no remote at all', '[core]\n\trepositoryformatversion = 0\n'],
])('does not misclassify %s as github', async (_label, config) => {
stubRepo(config);
await expect(detectRepoType('/repo')).resolves.toBe('git');
});
it('returns undefined when there is no .git', async () => {
vi.mocked(readFile).mockRejectedValue(enoent());
vi.mocked(readdir).mockRejectedValue(enoent());
await expect(detectRepoType('/not-a-repo')).resolves.toBeUndefined();
});
});
@@ -35,8 +35,8 @@ describe('Protocol', () => {
const urlObj = new URL(url);
const schemaParam = urlObj.searchParams.get('schema');
expect(schemaParam).toBeTruthy();
// URLSearchParams.get() 自动解码,所以这里得到的是解码后的JSON
expect(schemaParam).toContain('"'); // 解码后的引号
// URLSearchParams.get() auto-decodes, so we get the decoded JSON here
expect(schemaParam).toContain('"'); // decoded quotes
});
it('should generate valid RFC protocol URL for http type', () => {
@@ -114,7 +114,7 @@ describe('Protocol', () => {
expect(parsed?.params.marketId).toBe('lobehub');
expect(parsed?.originalUrl).toBe(url);
// 验证 schema 可以被解析
// Verify that the schema can be parsed
const parsedSchema = JSON.parse(parsed?.params.schema || '{}');
expect(parsedSchema).toEqual(schema);
});
@@ -177,7 +177,7 @@ describe('Protocol', () => {
expect(parsed?.params.id).toBe('special-chars');
expect(parsed?.params.type).toBe('mcp');
// 验证 schema 可以正确解析
// Verify that the schema can be parsed correctly
const parsedSchema = JSON.parse(parsed?.params.schema || '{}');
expect(parsedSchema).toEqual(schema);
});
+71
View File
@@ -0,0 +1,71 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { readdir } from 'fs-extra';
/**
* Resolve the actual `.git` directory for a working tree.
* Supports both standard layouts and worktree pointer files (`.git` as a regular file
* containing `gitdir: <path>`).
*/
export const resolveGitDir = async (dirPath: string): Promise<string | undefined> => {
const gitPath = path.join(dirPath, '.git');
try {
const content = await readFile(gitPath, 'utf8');
const worktreeMatch = /^gitdir:\s*(\S.*)$/m.exec(content.trim());
if (worktreeMatch) {
const resolved = worktreeMatch[1].trim();
return path.isAbsolute(resolved) ? resolved : path.resolve(dirPath, resolved);
}
} catch {
// `.git` is a directory (EISDIR) or missing — fall through
}
try {
const stat = await readdir(gitPath);
if (stat.length > 0) return gitPath;
} catch {
return undefined;
}
return undefined;
};
/**
* Resolve the common git dir — where shared state like `config` and
* `packed-refs` lives. For linked worktrees, `resolveGitDir` returns
* `.git/worktrees/<name>/` which has its own `HEAD` but no `config`;
* the `commondir` pointer inside it resolves to the main repo's gitdir.
*/
export const resolveCommonGitDir = async (dirPath: string): Promise<string | undefined> => {
const gitDir = await resolveGitDir(dirPath);
if (!gitDir) return undefined;
try {
const commondir = (await readFile(path.join(gitDir, 'commondir'), 'utf8')).trim();
if (!commondir) return gitDir;
return path.isAbsolute(commondir) ? commondir : path.resolve(gitDir, commondir);
} catch {
return gitDir;
}
};
// Match `github.com` only in a remote-URL host position: preceded by `@`, `/`,
// or line start (covers `git@github.com:`, `https://github.com/`,
// `ssh://git@github.com/`, etc.) and followed by `:` or `/`. Avoids matching
// look-alikes like `evilgithub.com` or `github.com.attacker.com`.
const GITHUB_REMOTE_HOST_RE = /(?:^|[@/])github\.com[:/]/m;
/**
* Classify a working tree as `git` (plain) / `github` (origin points at github.com) /
* `undefined` (not a git repo). Reads the shared gitdir's `config` so submodules and
* linked worktrees resolve the same as the main repo.
*/
export const detectRepoType = async (dirPath: string): Promise<'git' | 'github' | undefined> => {
const commonDir = await resolveCommonGitDir(dirPath);
if (!commonDir) return undefined;
try {
const config = await readFile(path.join(commonDir, 'config'), 'utf8');
if (GITHUB_REMOTE_HOST_RE.test(config)) return 'github';
return 'git';
} catch {
return undefined;
}
};
@@ -0,0 +1,9 @@
{
"name": "@lobechat/business-const",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"main": "./src/index.ts"
}
@@ -0,0 +1,6 @@
export const BRANDING_LOGO_URL = '';
export const BRANDING_NAME = 'LobeHub';
export const DEFAULT_EMBEDDING_PROVIDER = 'openai';
export const DEFAULT_MINI_PROVIDER = 'openai';
export const DEFAULT_PROVIDER = 'openai';
export const ORG_NAME = 'LobeHub';
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@lobechat/types",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"main": "./src/index.ts"
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Desktop isolated workspace stub.
*
* `@lobechat/types` is only consumed via `import type` in desktop code and in
* the `@lobechat/const` entrypoints it reaches (currently `desktopGlobalShortcuts`).
* Those specifiers are erased at build time, so this package has no runtime
* exports — we only need to surface the types that reach the desktop tsgo
* project. Keep these in sync with `packages/types/src/hotkey.ts`.
*/
export type DesktopHotkeyId = 'openSettings' | 'showApp';
export interface DesktopHotkeyItem {
id: DesktopHotkeyId;
keys: string;
nonEditable?: boolean;
}
export type DesktopHotkeyConfig = Record<DesktopHotkeyId, string>;
+44 -96
View File
@@ -1,21 +1,27 @@
[
{
"children": {
"improvements": [
"add agent task system database schema."
]
"fixes": ["fix minify cli.", "recent delete."]
},
"date": "2026-04-16",
"version": "2.1.51"
},
{
"children": {},
"date": "2026-04-16",
"version": "2.1.50"
},
{
"children": {
"improvements": ["add agent task system database schema."]
},
"date": "2026-03-26",
"version": "2.1.45"
},
{
"children": {
"fixes": [
"misc UI/UX improvements and bug fixes."
],
"improvements": [
"add image/video switch."
]
"fixes": ["misc UI/UX improvements and bug fixes."],
"improvements": ["add image/video switch."]
},
"date": "2026-03-20",
"version": "2.1.44"
@@ -47,27 +53,21 @@
},
{
"children": {
"improvements": [
"add api key hash column migration."
]
"improvements": ["add api key hash column migration."]
},
"date": "2026-03-09",
"version": "2.1.39"
},
{
"children": {
"fixes": [
"when use trustclient not register market m2m token."
]
"fixes": ["when use trustclient not register market m2m token."]
},
"date": "2026-03-06",
"version": "2.1.38"
},
{
"children": {
"improvements": [
"Update i18n."
]
"improvements": ["Update i18n."]
},
"date": "2026-02-10",
"version": "2.1.26"
@@ -79,9 +79,7 @@
},
{
"children": {
"fixes": [
"Fix multimodal content_part images rendered as base64 text."
]
"fixes": ["Fix multimodal content_part images rendered as base64 text."]
},
"date": "2026-02-09",
"version": "2.1.24"
@@ -91,18 +89,14 @@
"fixes": [
"Fix editor content missing when send error, use custom avatar for group chat in sidebar."
],
"improvements": [
"Update i18n."
]
"improvements": ["Update i18n."]
},
"date": "2026-02-09",
"version": "2.1.23"
},
{
"children": {
"fixes": [
"Register Notebook tool in server runtime."
]
"fixes": ["Register Notebook tool in server runtime."]
},
"date": "2026-02-08",
"version": "2.1.22"
@@ -127,9 +121,7 @@
},
{
"children": {
"fixes": [
"Fixed in community pluings tab the lobehub skills not display."
]
"fixes": ["Fixed in community pluings tab the lobehub skills not display."]
},
"date": "2026-02-06",
"version": "2.1.19"
@@ -146,27 +138,21 @@
},
{
"children": {
"fixes": [
"Add the preview publish to market button preview check."
]
"fixes": ["Add the preview publish to market button preview check."]
},
"date": "2026-02-04",
"version": "2.1.16"
},
{
"children": {
"fixes": [
"Fixed the agents list the show updateAt time error."
]
"fixes": ["Fixed the agents list the show updateAt time error."]
},
"date": "2026-02-04",
"version": "2.1.15"
},
{
"children": {
"fixes": [
"Fix cannot uncompressed messages."
]
"fixes": ["Fix cannot uncompressed messages."]
},
"date": "2026-02-04",
"version": "2.1.14"
@@ -183,9 +169,7 @@
},
{
"children": {
"fixes": [
"Hide password features when AUTH_DISABLE_EMAIL_PASSWORD is set."
]
"fixes": ["Hide password features when AUTH_DISABLE_EMAIL_PASSWORD is set."]
},
"date": "2026-02-02",
"version": "2.1.11"
@@ -197,54 +181,42 @@
},
{
"children": {
"fixes": [
"Use oauth2.link for generic OIDC provider account linking."
]
"fixes": ["Use oauth2.link for generic OIDC provider account linking."]
},
"date": "2026-02-02",
"version": "2.1.9"
},
{
"children": {
"improvements": [
"Improve tasks display."
]
"improvements": ["Improve tasks display."]
},
"date": "2026-02-01",
"version": "2.1.8"
},
{
"children": {
"fixes": [
"Add missing description parameter docs in Notebook system prompt."
]
"fixes": ["Add missing description parameter docs in Notebook system prompt."]
},
"date": "2026-02-01",
"version": "2.1.7"
},
{
"children": {
"improvements": [
"Improve local-system tool implement."
]
"improvements": ["Improve local-system tool implement."]
},
"date": "2026-02-01",
"version": "2.1.6"
},
{
"children": {
"fixes": [
"Slove the group member agents cant set skills problem."
]
"fixes": ["Slove the group member agents cant set skills problem."]
},
"date": "2026-01-31",
"version": "2.1.5"
},
{
"children": {
"improvements": [
"Update i18n, Update Kimi K2.5 & Qwen3 Max Thinking models."
]
"improvements": ["Update i18n, Update Kimi K2.5 & Qwen3 Max Thinking models."]
},
"date": "2026-01-31",
"version": "2.1.4"
@@ -256,63 +228,49 @@
},
{
"children": {
"fixes": [
"Fix feishu sso provider."
]
"fixes": ["Fix feishu sso provider."]
},
"date": "2026-01-30",
"version": "2.1.2"
},
{
"children": {
"fixes": [
"Correct desktop download URL path."
]
"fixes": ["Correct desktop download URL path."]
},
"date": "2026-01-30",
"version": "2.1.1"
},
{
"children": {
"features": [
"Refactor cron job UI and use runtime enableBusinessFeatures flag."
]
"features": ["Refactor cron job UI and use runtime enableBusinessFeatures flag."]
},
"date": "2026-01-30",
"version": "2.1.0"
},
{
"children": {
"improvements": [
"Fix usage table display issues."
]
"improvements": ["Fix usage table display issues."]
},
"date": "2026-01-29",
"version": "2.0.13"
},
{
"children": {
"fixes": [
"Group publish to market should set local group market identifer."
]
"fixes": ["Group publish to market should set local group market identifer."]
},
"date": "2026-01-29",
"version": "2.0.12"
},
{
"children": {
"improvements": [
"Fix group task render."
]
"improvements": ["Fix group task render."]
},
"date": "2026-01-29",
"version": "2.0.11"
},
{
"children": {
"fixes": [
"Add ExtendParamsTypeSchema for enhanced model settings."
]
"fixes": ["Add ExtendParamsTypeSchema for enhanced model settings."]
},
"date": "2026-01-29",
"version": "2.0.10"
@@ -324,9 +282,7 @@
},
{
"children": {
"fixes": [
"Fix inbox agent in mobile."
]
"fixes": ["Fix inbox agent in mobile."]
},
"date": "2026-01-28",
"version": "2.0.8"
@@ -338,27 +294,21 @@
},
{
"children": {
"fixes": [
"The klavis in onboarding connect timeout fixed."
]
"fixes": ["The klavis in onboarding connect timeout fixed."]
},
"date": "2026-01-27",
"version": "2.0.6"
},
{
"children": {
"fixes": [
"Update the artifact prompt."
]
"fixes": ["Update the artifact prompt."]
},
"date": "2026-01-27",
"version": "2.0.5"
},
{
"children": {
"fixes": [
"Rename docker image and update docs for v2."
]
"fixes": ["Rename docker image and update docs for v2."]
},
"date": "2026-01-27",
"version": "2.0.4"
@@ -374,9 +324,7 @@
},
{
"children": {
"fixes": [
"Slove the recentTopicLinkError."
]
"fixes": ["Slove the recentTopicLinkError."]
},
"date": "2026-01-27",
"version": "2.0.2"
+23 -9
View File
@@ -1,9 +1,6 @@
---
title: LobeHub Plugin Ecosystem - Functionality Extensions and Development Resources
description: >-
Discover how the LobeHub plugin ecosystem enhances the utility and flexibility
of the LobeHub assistant, along with the development resources and plugin
development guidelines provided.
title: 'Plugin System: Extend Your Agents with Community Skills'
description: LobeHub now supports a plugin ecosystem that lets Agents access real-time information, interact with external services, and handle specialized tasks without leaving the conversation.
tags:
- LobeHub
- Plugins
@@ -13,12 +10,29 @@ tags:
# Supported Plugin System
The LobeHub plugin ecosystem is a significant extension of its core functionalities, greatly enhancing the utility and flexibility of the LobeHub assistant.
LobeHub now supports plugins that extend what your Agents can do. Instead of being limited to built-in capabilities, Agents can now pull live data, interact with external platforms, and handle specialized workflows through community-built extensions.
<Video src="/blog/assets/28616219/f29475a3-f346-4196-a435-41a6373ab9e2.mp4" />
By leveraging plugins, the LobeHub assistants are capable of accessing and processing real-time information, such as searching online for data and providing users with timely and relevant insights.
## Access real-time information
Moreover, these plugins are not solely limited to news aggregation; they can also extend to other practical functionalities, such as quickly retrieving documents, generating images, obtaining data from various platforms such as Bilibili and Steam, and interacting with an array of third-party services.
Previously, conversations were limited to the knowledge cutoff of the underlying model. Now, with plugins like web search, your Agents can fetch current information—news, documentation, stock prices, or weather—right when you need it.
To learn more, please refer to the [Plugin Usage](/en/docs/usage/plugins/basic). Additionally, quality voice options (OpenAI Audio, Microsoft Edge Speech) are available to cater to users from different regions and cultural backgrounds. Users can select suitable voices based on personal preferences or specific situations, providing a personalized communication experience.
Use plugins to:
- Run web searches and get up-to-date answers
- Query documentation sites and technical references
- Retrieve platform data from services like Bilibili or Steam
- Generate images on demand during a conversation
## Community-powered flexibility
The plugin system is designed to grow with community contributions. Developers can build and share custom plugins that add new capabilities to any Agent. Users simply enable the plugins they need for their specific workflow.
This means your Agents become more specialized over time. A coding assistant might enable documentation search and code execution plugins. A creative assistant might use image generation and content research tools. The same underlying Agent adapts to different contexts through its enabled plugins.
## Voice options for natural interaction
Alongside plugin capabilities, LobeHub now offers quality voice synthesis options including OpenAI Audio and Microsoft Edge Speech. Choose a voice that matches your preference or scenario for more personalized interactions.
Learn more about plugin usage in our [documentation](/en/docs/usage/plugins/basic).
@@ -1,6 +1,6 @@
---
title: LobeHub 插件生态系统 - 功能扩展与开发资源
description: 了解 LobeHub 插件生态系统如何增强 LobeHub 助手的实用性和灵活性,以及提供的开发资源和插件开发指南
title: '插件系统:用社区技能扩展你的助理'
description: LobeHub 现已支持插件生态,让助理能够获取实时信息、与外部服务交互,并在对话中处理各种专业任务
tags:
- LobeHub
- 插件系统
@@ -10,12 +10,29 @@ tags:
# 支持插件系统
LobeHub 的插件生态系统是其核心功能的重要扩展,它极大地增强了 LobeHub 助手的实用性和灵活性
LobeHub 现已支持插件功能,大幅扩展了助理的能力边界。借助社区开发的插件,助理可以获取实时数据、与外部平台交互,并处理各种专业工作流,而无需离开对话界面
<Video src="/blog/assets/28616219/f29475a3-f346-4196-a435-41a6373ab9e2.mp4" />
通过利用插件,LobeHub 的助手们能够实现实时信息的获取和处理,例如搜索网络信息,为用户提供即时且相关的资讯。
## 获取实时信息
此外,这些插件不仅局限于新闻聚合,还可以扩展到其他实用的功能,如快速检索文档、生成图片、获取 Bilibili 、Steam 等各种平台数据,以及与其他各式各样的第三方服务交互
以往,对话内容受限于模型本身的知识截止日期。现在,通过联网搜索等插件,助理可以实时获取最新资讯 —— 无论是新闻、技术文档、股价还是天气信息,都能在需要时即时查询
通过查看 [插件使用](/zh/docs/usage/plugins/basic) 了解更多。质的声音选项 (OpenAI Audio, Microsoft Edge Speech),以满足不同地域和文化背景用户的需求。用户可以根据个人喜好或者特定场景来选择合适的语音,从而获得个性化的交流体验。
你可以使用插件来:
- 执行网页搜索,获得最新的答案
- 查询技术文档和参考资料
- 获取 Bilibili、Steam 等平台的数据
- 在对话过程中按需生成图片
## 社区驱动的灵活性
插件系统设计为随社区贡献而不断成长。开发者可以构建并分享自定义插件,为任何助理添加新能力。用户只需根据具体工作流启用所需插件即可。
这意味着你的助理可以变得更加专业化。编程助理可以启用文档搜索和代码执行插件,创意助理可以使用图像生成和内容研究工具。同一个助理通过启用不同的插件,就能适应不同的使用场景。
## 自然的语音交互
除了插件能力之外,LobeHub 还提供了高品质的语音合成选项,包括 OpenAI Audio 和 Microsoft Edge Speech。你可以根据个人偏好或具体场景选择合适的声音,获得更个性化的交互体验。
了解更多插件使用方法,请查看[文档](/zh/docs/usage/plugins/basic)。
+23 -10
View File
@@ -1,12 +1,6 @@
---
title: >-
LobeHub Supports Multimodal Interaction: Visual Recognition Enhances
Intelligent Dialogue
description: >-
LobeHub supports various large language models with visual recognition
capabilities, allowing users to upload or drag and drop images. The assistant
will recognize the content and engage in intelligent dialogue, creating a more
intelligent and diverse chat environment.
title: 'Visual Recognition: Chat With Images, Not Just Text'
description: LobeHub now supports multimodal models including GPT-4 Vision, Google Gemini Pro Vision, and GLM-4 Vision. Upload or drag images into conversations and your Agent will understand and respond to visual content.
tags:
- Visual Recognition
- LobeHub
@@ -17,6 +11,25 @@ tags:
# Supported Models for Visual Recognition
LobeHub now supports several large language models with visual recognition capabilities, including OpenAI's [`gpt-4-vision`](https://platform.openai.com/docs/guides/vision), Google Gemini Pro vision, and Zhiyuan GLM-4 Vision. This empowers LobeHub with multimodal interaction capabilities. Users can effortlessly upload images or drag and drop them into the chat window, where the assistant can recognize the image content and engage in intelligent dialogue, building a smarter and more diverse chat experience.
Conversations in LobeHub are no longer limited to text. We now support several large language models with visual recognition capabilities, including OpenAI's [`gpt-4-vision`](https://platform.openai.com/docs/guides/vision), Google Gemini Pro Vision, and Zhiyuan GLM-4 Vision.
This feature opens up new avenues for interaction, allowing communication that extends beyond text to include rich visual elements. Whether sharing images during everyday use or interpreting graphics in specific industries, the assistant delivers an exceptional conversational experience. Additionally, we have carefully selected a range of high-quality voice options (OpenAI Audio, Microsoft Edge Speech) to cater to users from different regions and cultural backgrounds. Users can choose a suitable voice based on personal preferences or specific contexts, thus receiving a more personalized communication experience.
## Share images naturally
Upload an image or drag it directly into the chat window, and your Agent can understand the visual content and continue the discussion in context. This works for screenshots, photos, diagrams, or any visual reference you need to share.
This brings a more natural multimodal experience to both everyday and professional scenarios:
- Share photos from your day and discuss them
- Upload UI screenshots for design feedback
- Share diagrams and get explanations
- Reference visual content without describing it in words
## Context-aware visual understanding
The assistant doesn't just see the image—it understands it within the ongoing conversation. Ask follow-up questions about specific details, compare multiple images, or use visuals as reference material for complex discussions.
For specialized fields, this means clearer context and more practical responses. Medical imaging discussions, architectural reviews, or technical diagram analysis all become more natural when both parties can see the same visual reference.
## Voice options for personalized interaction
To better serve users across regions and preferences, we've also added quality voice options from OpenAI Audio and Microsoft Edge Speech. Choose a voice that fits your style or scenario for more personalized interactions.
@@ -1,6 +1,6 @@
---
title: LobeHub 支持多模态交互:视觉识别助力智能对话
description: LobeHub 支持多种具有视觉识别能力的大语言模型,用户可上传或拖拽图片,助手将识别内容并展开智能对话,打造更智能、多元化的聊天场景
title: '视觉识别:与图片对话,不只是文字'
description: LobeHub 现已支持多模态模型,包括 GPT-4 Vision、Google Gemini Pro Vision 和 GLM-4 Vision。上传或拖拽图片到对话中,助理将理解视觉内容并作出回应
tags:
- 视觉识别
- 多模态交互
@@ -11,6 +11,25 @@ tags:
# 支持模型视觉识别
LobeHub 已经支持 OpenAI 的 [`gpt-4-vision`](https://platform.openai.com/docs/guides/vision) 、Google Gemini Pro vision智谱 GLM-4 Vision 等具有视觉识别能力的大语言模型,这使得 LobeHub 具备了多模态交互的能力。用户可以轻松上传图片或者拖拽图片到对话框中,助手将能够识别图片内容,并在此基础上进行智能对话,构建更智能、更多元化的聊天场景
LobeHub 的对话不再局限于纯文字。我们现已支持多个具备视觉识别能力的大语言模型,包括 OpenAI 的 [`gpt-4-vision`](https://platform.openai.com/docs/guides/vision)、Google Gemini Pro Vision,以及智谱 GLM-4 Vision。
这一特性打开了新的互动方式,使得交流不再局限于文字,而是可以涵盖丰富的视觉元素。无论是日常使用中的图片分享,还是在特定行业内的图像解读,助手都能提供出色的对话体验。,我们精心挑选了一系列高品质的声音选项 (OpenAI Audio, Microsoft Edge Speech),以满足不同地域和文化背景用户的需求。用户可以根据个人喜好或者特定场景来选择合适的语音,从而获得个性化的交流体验。
## 自然地分享图片
上传图片或直接拖拽到对话框,助理就能理解视觉内容并基于上下文继续对话。无论是截图、照片、图表还是任何视觉参考,都能轻松分享。
这为日常场景和专业场景带来了更自然的多模态体验:
- 分享生活中的照片并展开讨论
- 上传界面截图获取设计反馈
- 分享图表并获得解读
- 引用视觉内容而无需用文字描述
## 上下文感知的视觉理解
助理不只是 "看见" 图片 —— 它能在持续对话中理解图片内容。你可以针对特定细节追问、比较多张图片,或将视觉资料作为复杂讨论的参考。
对于专业领域,这意味着更清晰的上下文和更实用的回复。医学影像讨论、建筑方案评审或技术图表分析,当双方都能看到相同的视觉参考时,交流变得更加自然高效。
## 个性化的语音交互
为了更好地服务不同地区和偏好的用户,我们还加入了 OpenAI Audio 和 Microsoft Edge Speech 的高品质语音选项。选择符合你风格或场景的声音,获得更个性化的交互体验。
+22 -7
View File
@@ -1,9 +1,6 @@
---
title: LobeHub Text-to-Image Generation Technology
description: >-
LobeHub supports Text-to-Speech (TTS) and Speech-to-Text (STT) technologies,
offering high-quality voice options for a personalized communication
experience. Learn more about Lobe TTS Toolkit.
title: 'Voice Conversations: Talk Naturally With Your Agents'
description: LobeHub now supports Text-to-Speech (TTS) and Speech-to-Text (STT), enabling natural voice interactions. Speak with your Agents and hear responses in clear, personalized voices.
tags:
- TTS
- STT
@@ -14,6 +11,24 @@ tags:
# Supporting TTS & STT Voice Conversations
LobeHub supports Text-to-Speech (TTS) and Speech-to-Text (STT) technologies, allowing our application to transform textual information into clear voice output. Users can interact with our conversational agents as if they were talking to a real person. There are various voice options for users to choose from, providing the right audio source for their assistant. Additionally, for those who prefer auditory learning or seek to gain information while on the go, TTS offers an excellent solution.
LobeHub now supports Text-to-Speech (TTS) and Speech-to-Text (STT), turning typed conversations into natural voice interactions. You can speak with your Agents and hear their responses, making the experience closer to talking with a real person.
In LobeHub, we have carefully curated a selection of high-quality voice options (OpenAI Audio, Microsoft Edge Speech) to cater to users from different regions and cultural backgrounds. Users can select suitable voices based on personal preferences or specific scenarios, thus achieving a personalized communication experience.
## Natural voice interaction
With TTS, your Agents can read responses aloud in clear, natural-sounding voices. With STT, you can dictate messages instead of typing. Together, they enable hands-free interaction—useful when you're multitasking, on the move, or simply prefer speaking to typing.
This is especially helpful for:
- Auditory learners who process information better by hearing
- Users who want to stay productive while commuting or away from a keyboard
- Anyone who finds voice more accessible or convenient than text
## Personalized voice selection
Different Agents can have different voices. Choose a voice that matches each Agent's personality or purpose. A professional assistant might use a calm, measured tone. A creative collaborator might sound more expressive.
We've curated high-quality voices from OpenAI Audio and Microsoft Edge Speech to serve users across regions and preferences. Select the voice that fits your usage style or scenario.
## A complete communication loop
Voice support closes the gap between human and AI interaction styles. Speak naturally, hear responses aloud, and maintain context just like you would in a spoken conversation. The rest of LobeHub's features—plugins, multimodal support, context management—work seamlessly alongside voice mode.
+22 -4
View File
@@ -1,6 +1,6 @@
---
title: LobeHub 文生图:文本转图片生成技术
description: LobeHub 支持文字转语音(TTS)和语音转文字(STT)技术,提供高品质声音选项,个性化交流体验。了解更多关于 Lobe TTS 工具包
title: '语音会话:与你的助理自然对话'
description: LobeHub 现已支持文字转语音(TTS)和语音转文字(STT),实现自然的语音交互。与助理对话并听到清晰、个性化的语音回复
tags:
- TTS
- STT
@@ -12,6 +12,24 @@ tags:
# 支持 TTS & STT 语音会话
LobeHub 支持文字转语音(Text-to-SpeechTTS)和语音转文字(Speech-to-Text,STT)技术,我们的应用能够将文本信息转化为清晰的语音输出,用户可以像与真人交谈一样与我们的对话代理进行交流。用户可以从多种声音中选择,给助手搭配合适的音源。 同时,对于那些倾向于听觉学习或者想要在忙碌中获取信息的用户来说,TTS 提供了一个极佳的解决方案
LobeHub 现已支持文字转语音(TTS)和语音转文字(STT),将文字对话转化为自然的语音交互。你可以与助理对话并听到它们的回复,体验更接近与真人交流
在 LobeHub 中,我们精心挑选了一系列高品质的声音选项 (OpenAI Audio, Microsoft Edge Speech),以满足不同地域和文化背景用户的需求。用户可以根据个人喜好或者特定场景来选择合适的语音,从而获得个性化的交流体验。
## 自然的语音交互
借助 TTS,助理可以用清晰自然的声音朗读回复。借助 STT,你可以用语音输入代替打字。两者结合,实现了免提交互 —— 当你正在处理其他事务、在通勤途中,或单纯更喜欢说话时,这项功能尤其实用。
语音功能特别适合:
- 听觉型学习者,通过聆听更好地处理信息
- 希望在通勤或远离键盘时保持高效的用户
- 觉得语音比文字更便捷或更易用的用户
## 个性化的声音选择
不同的助理可以配备不同的声音。你可以根据每个助理的性格或用途选择合适的声音。专业的助理可以使用沉稳、从容的语调,创意型的助理则可以更加富有表现力。
我们精选了 OpenAI Audio 和 Microsoft Edge Speech 的高品质声音选项,以服务不同地区和偏好的用户。选择最符合你使用风格或场景的语音。
## 完整的交流闭环
语音支持弥合了人类与 AI 交互方式之间的差距。自然地说话,听到语音回复,并像在真实对话中一样保持上下文。LobeHub 的其他功能 —— 插件、多模态支持、上下文管理 —— 都能与语音模式无缝协作。
+17 -8
View File
@@ -1,11 +1,6 @@
---
title: 'LobeHub Text-to-Image: Text-to-Image Generation Technology'
description: >-
LobeHub now supports the latest text-to-image generation technology, allowing
users to directly invoke the text-to-image tool during conversations with the
assistant for creative purposes. By utilizing AI tools such as DALL-E 3,
MidJourney, and Pollinations, assistants can turn your ideas into images,
making the creative process more intimate and immersive.
title: 'Text-to-Image: Create Visuals Directly in Chat'
description: LobeHub now supports text-to-image generation. Invoke DALL-E 3, MidJourney, or Pollinations directly during conversations to turn your ideas into images without leaving the chat.
tags:
- Text-to-Image
- LobeHub
@@ -16,4 +11,18 @@ tags:
# Support for Text-to-Image Generation
The latest text-to-image generation technology is now supported, enabling LobeHub users to directly use the text-to-image tool during conversations with their assistant. By harnessing the capabilities of AI tools like [`DALL-E 3`](https://openai.com/dall-e-3), [`MidJourney`](https://www.midjourney.com/), and [`Pollinations`](https://pollinations.ai/), assistants can now transform your ideas into images. This allows for a more intimate and immersive creative process.
LobeHub now supports text-to-image generation, so you can create images directly while chatting with your Agents. With tools like [`DALL-E 3`](https://openai.com/dall-e-3), [`MidJourney`](https://www.midjourney.com/), and [`Pollinations`](https://pollinations.ai/), your Agents can turn your descriptions into visuals within the same conversation flow.
## Creative workflow without switching tools
Previously, generating AI images meant leaving your conversation, opening a separate tool, writing a prompt, waiting for results, then copying the image back. Now you simply describe what you want, and your Agent produces the image right there in the chat.
This keeps creative momentum flowing. Iterate on ideas quickly—request adjustments, explore variations, or refine descriptions—all without context switching. The conversation history maintains your creative direction, so you can reference previous ideas and build on them.
## Private and immersive creation
Image generation happens within your existing conversation, keeping your creative process contained and private. No need to manage separate accounts or jump between platforms. Your prompts, iterations, and final images stay in one place, organized alongside the rest of your discussion.
## Multiple generation options
Different tools excel at different styles. DALL-E 3 offers detailed, precise renders. MidJourney produces artistic, atmospheric results. Pollinations provides fast, accessible generation. Your Agent can help you choose and use the right tool for each creative task.
+17 -5
View File
@@ -1,8 +1,6 @@
---
title: LobeHub 文生图:文本转图片生成技术
description: >-
LobeHub 现在支持最新的文本到图片生成技术,让用户可以在与助手对话中直接调用文生图工具进行创作。利用 DALL-E 3、MidJourney 和
Pollinations 等 AI 工具,助手们可以将你的想法转化为图像,让创作过程更私密和沉浸式。
title: '文生图:在对话中直接创作视觉内容'
description: LobeHub 现已支持文本到图片生成。在对话中直接调用 DALL-E 3、MidJourney 或 Pollinations,无需离开聊天界面即可将想法转化为图像。
tags:
- Text to Image
- 文生图
@@ -11,4 +9,18 @@ tags:
# 支持 Text to Image 文生图
现已支持最新的文本到图片生成技术,LobeHub 现在能够让用户在与助对话直接调用文成图工具进行创作。通过利用 [`DALL-E 3`](https://openai.com/dall-e-3)、[`MidJourney`](https://www.midjourney.com/) 和 [`Pollinations`](https://pollinations.ai/) 等 AI 工具的能力, 助手们现在可以将你的想法转化为图像。同时可以更私密和沉浸式的完成你的创造过程
LobeHub 现已支持文生图功能,你可以在与助对话直接创作图像。借助 [`DALL-E 3`](https://openai.com/dall-e-3)、[`MidJourney`](https://www.midjourney.com/) 和 [`Pollinations`](https://pollinations.ai/) 等工具,助理可以在同一段对话中将你的描述转化为视觉作品
## 无需切换工具的创意工作流
以往,生成 AI 图像意味着离开对话、打开另一个工具、编写提示词、等待生成结果,然后再把图片复制回来。现在,你只需描述想要什么,助理就能直接在对话中生成图像。
这让创意节奏保持流畅。快速迭代想法 —— 要求调整、探索变体、优化描述 —— 都无需切换上下文。对话历史会保留你的创作方向,让你可以引用之前的想法并在此基础上继续发展。
## 私密且沉浸的创作体验
图像生成在现有对话中完成,让创作过程保持封闭和私密。无需管理多个账号或在不同平台之间跳转。你的提示词、迭代过程和最终图像都保存在同一个地方,与讨论内容一起有序管理。
## 多种生成选项
不同工具擅长不同风格。DALL-E 3 提供精细、准确的渲染效果,MidJourney 产出富有艺术感和氛围感的作品,Pollinations 提供快速、便捷的生成能力。你的助理可以帮你为每个创作任务选择和调用合适的工具。
+30 -17
View File
@@ -1,8 +1,9 @@
---
title: LobeHub Supports Multi-User Management with Clerk and Next-Auth
title: Authentication That Adapts to Your Stack
description: >-
LobeHub offers various user authentication and management solutions, including
Clerk and Next-Auth, to meet the diverse needs of different users.
LobeHub now supports both Clerk and Next-Auth, giving teams flexibility to
choose the authentication approach that fits their deployment model and
security requirements.
tags:
- User Management
- Next-Auth
@@ -11,24 +12,36 @@ tags:
- Multi-Factor Authentication
---
# Support for Multi-User Management with Clerk and Next-Auth
# Authentication That Adapts to Your Stack
In modern applications, user management and authentication are crucial features. To cater to the diverse needs of users, LobeHub provides two primary user authentication and management solutions: `next-auth` and `Clerk`. Whether you're looking for simple user registration and login or need more advanced multi-factor authentication and user management, LobeHub can flexibly accommodate your requirements.
Every product needs reliable sign-in, but not every team has the same requirements. Some need to get up and running quickly with social logins. Others need enterprise-grade controls from day one. LobeHub now supports both paths by integrating with next-auth and Clerk.
## Next-Auth: A Flexible and Powerful Authentication Library
This gives teams the freedom to start simple and upgrade security when the time is right—without rethinking their entire auth architecture.
LobeHub integrates `next-auth`, a flexible and powerful authentication library that supports various authentication methods, including OAuth, email login, and credential-based login. With `next-auth`, you can easily implement the following features:
## Next-Auth: Start Fast, Stay Flexible
- **User Registration and Login**: Supports multiple authentication methods to meet different user needs.
- **Session Management**: Efficiently manage user sessions to ensure security.
- **Social Login**: Quick login options for various social media platforms.
- **Data Security**: Protects user data privacy and security.
Next-Auth provides a straightforward authentication layer for teams that want to ship quickly. It handles the essentials: OAuth from major providers, email-based login, and credential-based flows, all without managing a separate user service.
## Clerk: A Modern User Management Platform
Use this when you need:
For users who require more advanced user management capabilities, LobeHub also supports [Clerk](https://clerk.com), a modern user management platform. Clerk offers a richer set of features, helping you achieve enhanced security and flexibility:
- Quick setup with social providers like GitHub or Google
- Session management that just works
- Full control over the sign-in UI and flow
- Privacy-friendly auth that keeps user data in your infrastructure
- **Multi-Factor Authentication (MFA)**: Provides an additional layer of security.
- **User Profile Management**: Easily manage user information and settings.
- **Login Activity Monitoring**: Real-time monitoring of user login activities to ensure account security.
- **Scalability**: Supports complex user management needs.
## Clerk: Enterprise-Ready Identity
When you need more than sign-in—multi-factor authentication, user profiles, and login activity monitoring—Clerk provides those capabilities out of the box. It's a managed identity platform that scales with your product.
Switch to Clerk when you need:
- MFA for sensitive accounts or compliance requirements
- Built-in user profile and account management UI
- Real-time login activity tracking
- Scalable identity infrastructure without operational overhead
## Improvements and fixes
- Added support for next-auth v5 beta with improved session handling
- Fixed redirect loop issues when using custom sign-in pages
- Improved error messages for failed OAuth connections
+29 -16
View File
@@ -1,6 +1,7 @@
---
title: LobeHub 支持 Clerk 与 Next-Auth 多用户管理支持
description: LobeHub 提供 Clerk 和 Next-Auth 等多种用户认证和管理方案,以满足不同用户的需求。
title: 灵活适配的认证体系:Clerk 与 Next-Auth 双方案支持
description: >-
LobeHub 现已支持 Clerk 和 Next-Auth 两种认证方案,让团队可以根据部署模式和安全需求选择最适合的身份验证方式。
tags:
- 用户管理
- 身份验证
@@ -9,24 +10,36 @@ tags:
- 多因素认证
---
# 支持 Clerk 与 Next-Auth 多用户管理支持
# 灵活适配的认证体系:Clerk 与 Next-Auth 双方案支持
在现代应用中,用户管理和身份验证是至关重要的功能。为满足不同用户的多样化需求,LobeHub 提供了两种主要的用户认证和管理方案:`next-auth``Clerk`。无论您是追求简便的用户注册登录,还是需要更高级的多因素认证和用户管理,LobeHub 都可以灵活实现
每个产品都需要可靠的登录系统,但不同团队的起点并不相同。有些团队希望快速接入社交登录上线,有些则需要企业级的安全管控。LobeHub 现在同时支持两种路径 —— 集成 next-auth 和 Clerk,满足不同阶段的需求
## next-auth:灵活且强大的身份验证库
这让团队可以自由选择:先以简单方案快速启动,待业务需要时再升级安全能力,无需重构整个认证架构。
LobeHub 集成了 `next-auth`,一个灵活且强大的身份验证库,支持多种身份验证方式,包括 OAuth、邮件登录、凭证登录等。通过 `next-auth`,您可以轻松实现以下功能:
## Next-Auth:快速启动,灵活可控
- **用户注册和登录**:支持多种认证方式,满足不同用户的需求
- **会话管理**:高效管理用户会话,确保安全性。
- **社交登录**:支持多种社交平台的快捷登录。
- **数据安全**:保障用户数据的安全性和隐私性。
Next-Auth 为希望快速交付的团队提供了轻量级认证层。它覆盖核心能力:主流 OAuth 提供商、邮箱登录、凭证登录,无需管理独立用户服务
## Clerk:现代化用户管理平台
适用场景:
对于需要更高级用户管理功能的用户,LobeHub 还支持 [Clerk](https://clerk.com) ,一个现代化的用户管理平台。Clerk 提供了更丰富的功能,帮助您实现更高的安全性和灵活性:
- 需要快速接入 GitHub、Google 等社交登录
- 开箱即用的会话管理
- 对登录界面和流程的完全控制
- 用户数据保留在自有基础设施中的隐私友好方案
- **多因素认证 (MFA)**:提供更高的安全保障。
- **用户配置文件管理**:便捷管理用户信息和配置。
- **登录活动监控**:实时监控用户登录活动,确保账户安全
- **扩展性**:支持复杂的用户管理需求。
## Clerk:企业级身份管理
当你需要的不仅是登录 —— 多因素认证、用户档案管理、登录行为监控 ——Clerk 提供开箱即用的完整能力。作为托管身份平台,它随产品规模自动扩展
适用场景:
- 敏感账户需要 MFA 或满足合规要求
- 需要内置的用户资料与账户管理界面
- 实时监控登录活动
- 免运维的高扩展性身份基础设施
## 体验优化与修复
- 新增 next-auth v5 beta 支持,改进会话处理机制
- 修复自定义登录页面导致的重定向循环问题
- 优化 OAuth 连接失败时的错误提示信息
+27 -10
View File
@@ -1,6 +1,8 @@
---
title: LobeHub Supports Ollama for Local Large Language Model (LLM) Calls
description: LobeHub v0.127.0 supports using Ollama to call local large language models.
title: Run Local Models Alongside Cloud AIs
description: >-
LobeHub v0.127.0 adds Ollama support, letting you run local large language
models with the same interface you use for cloud providers.
tags:
- Ollama AI
- LobeHub
@@ -9,20 +11,35 @@ tags:
- GPT-4
---
# Support for Ollama Calls to Local Large Language Models 🦙
# Run Local Models Alongside Cloud AIs
With the release of LobeHub v0.127.0, we're excited to introduce a fantastic new feature—Ollama AI support! 🤯 Thanks to the robust infrastructure provided by [Ollama AI](https://ollama.ai/) and the [efforts of the community](https://github.com/lobehub/lobe-chat/pull/1265), you can now interact with local LLMs (Large Language Models) within LobeHub! 🤩
Cloud models are powerful, but sometimes you need data to stay local. Maybe it's a sensitive project. Maybe you want to experiment without API costs. Maybe you just like the idea of owning the entire stack. LobeHub v0.127.0 now supports Ollama, giving you the same chat experience whether your model lives in the cloud or on your machine.
We are thrilled to unveil this revolutionary feature to all LobeHub users at this special moment. The integration of Ollama AI not only represents a significant leap in our technology but also reaffirms our commitment to continuously seek more efficient and intelligent ways of communication with our users.
No separate interface to learn. No workflow fragmentation. Just point LobeHub at your local Ollama instance and start chatting.
## 💡 How to Start a Conversation with Local LLMs?
## Connect Your Local Models in One Line
If you're facing challenges with private deployments, we strongly recommend trying out the LobeHub Cloud service. We offer comprehensive model support to help you easily embark on your AI conversation journey.
Experience the newly upgraded LobeHub v1.6 and feel the powerful conversational capabilities brought by GPT-4!
Getting started is straightforward. If you already have Ollama running, connect LobeHub with a single Docker command:
```bash
docker run -d -p 3210:3210 -e OLLAMA_PROXY_URL=http://host.docker.internal:11434/v1 lobehub/lobe-chat
```
Yes, it's that simple! 🤩 You don't need to go through complicated configurations or worry about intricate installation processes. We've prepared everything for you; just one command is all it takes to start deep conversations with local AI.
That's it. LobeHub detects your local models and makes them available in the same model switcher you use for GPT-4, Claude, and others. Mix cloud and local models in the same workspace depending on what each conversation needs.
## When to Use Local Models
- **Privacy-first work**: Keep sensitive conversations on your machine
- **Cost control**: No per-token charges for experimentation
- **Offline access**: Continue working without internet connectivity
- **Model testing**: Evaluate open-source models before production deployment
## Improvements and fixes
- Added automatic model discovery from Ollama endpoints
- Fixed streaming response handling for local model compatibility
- Improved error handling when Ollama service is unreachable
## Credits
Huge thanks to [the community contributor](https://github.com/lobehub/lobe-chat/pull/1265) who made Ollama integration possible, and to the Ollama team for building accessible local AI infrastructure.
+26 -10
View File
@@ -1,6 +1,7 @@
---
title: LobeHub 支持 Ollama 调用本地大语言模型(LLM)
description: LobeHub vLobeHub v0.127.0 支持 Ollama 调用本地大语言模型。
title: 本地模型与云端 AI 并行使用
description: >-
LobeHub v0.127.0 新增 Ollama 支持,让你可以用与云端模型相同的界面运行本地大语言模型。
tags:
- Ollama AI
- LobeHub
@@ -8,20 +9,35 @@ tags:
- AI 对话
---
# 支持 Ollama 调用本地大语言模型 🦙
# 本地模型与云端 AI 并行使用
随着 LobeHub v0.127.0 的发布,我们迎来了一个激动人心的特性 —— Ollama AI 支持!🤯 在 [Ollama AI](https://ollama.ai/) 强大的基础设施和 [社区的共同努力](https://github.com/lobehub/lobe-chat/pull/1265) 下,现在您可以在 LobeHub 中与本地 LLM (Large Language Model) 进行交流了!🤩
云端模型固然强大,但有时你需要数据留在本地。可能是敏感项目,可能是想免去 API 费用做实验,也可能只是希望完全掌控整个技术栈。LobeHub v0.127.0 现已支持 Ollama,无论模型运行在云端还是本地机器,你都能获得一致的对话体验。
我们非常高兴能在这个特别的时刻,向所有 LobeHub 用户介绍这项革命性的特性。Ollama AI 的集成不仅标志着我们技术上的一个巨大飞跃,更是向用户承诺,我们将不断追求更高效、更智能的沟通方式
无需学习新界面,无需割裂工作流程。将 LobeHub 指向你的 Ollama 实例,即可开始对话
## 💡 如何启动与本地 LLM 的对话?
## 一行命令连接本地模型
如果您在私有化部署方面遇到困难,强烈推荐尝试 LobeHub Cloud 服务。我们提供全方位的模型支持,让您轻松开启 AI 对话之旅。
赶快来体验全新升级的 LobeHub v1.6,感受 GPT-4 带来的强大对话能力!
启动过程非常简单。如果你已运行 Ollama,只需一条 Docker 命令即可连接:
```bash
docker run -d -p 3210:3210 -e OLLAMA_PROXY_URL=http://host.docker.internal:11434/v1 lobehub/lobe-chat
```
是的,就是这么简单!🤩 您不需要进行繁杂的配置,也不必担心复杂的安装过程。我们已经为您准备好了一切,只需一行命令,即可开启与本地 AI 的深度对话
仅此而已。LobeHub 会自动检测本地模型,并在你切换 GPT-4、Claude 等模型的同一处列出它们。根据每次对话的需求,自由混用云端和本地模型
## 本地模型的适用场景
- **隐私优先工作**:敏感对话全程留在本地
- **成本控制**:实验性使用无需按 token 付费
- **离线使用**:无网络连接时仍可继续工作
- **模型测试**:生产部署前评估开源模型效果
## 体验优化与修复
- 新增 Ollama 端点自动模型发现功能
- 修复本地模型兼容性的流式响应处理问题
- 优化 Ollama 服务不可达时的错误提示
## 致谢
衷心感谢实现 Ollama 集成的[社区贡献者](https://github.com/lobehub/lobe-chat/pull/1265),以及 Ollama 团队打造的易用本地 AI 基础设施。
+45 -12
View File
@@ -1,9 +1,9 @@
---
title: 'LobeHub 1.0: New Architecture and New Possibilities'
title: 'LobeHub 1.0: A New Foundation for Persistent, Multi-User Workspaces'
description: >-
LobeHub 1.0 brings a brand-new architecture and features for server-side
databases and user authentication management, opening up new possibilities. On
this basis, LobeHub Cloud has entered beta testing.
LobeHub 1.0 introduces server-side database support and comprehensive user
management, enabling knowledge bases, cross-device sync, and team
collaboration. LobeHub Cloud enters beta with these capabilities built-in.
tags:
- LobeHub
- Version 1.0
@@ -12,18 +12,51 @@ tags:
- Cloud Beta Testing
---
# LobeHub 1.0: New Architecture and New Possibilities
# LobeHub 1.0: A New Foundation for Persistent, Multi-User Workspaces
Since announcing our move towards version 1.0 in March, weve been busy upgrading every aspect of our platform. After two months of intensive development, we are excited to announce the official release of LobeHub 1.0! Lets take a look at our new features.
Since March, we've been rebuilding LobeHub from the ground up. Two months later, 1.0 is here. This isn't just an incremental update—it's a new architecture that enables the capabilities users have been asking for most.
## Server-Side Database Support
The 0.x era was defined by browser storage. Fast, simple, but limited. You couldn't sync across devices, build knowledge bases, or share agents with a team. Every session started fresh. LobeHub 1.0 changes that foundation.
The most significant feature of LobeHub 1.0 is the support for server-side databases. In the 0.x era, the lack of persistent storage on the server side made it challenging, if not impossible, to implement many features that users urgently needed, such as knowledge bases, cross-device synchronization, and private assistant markets.
## Server-Side Database: Data That Persists and Travels
## User Authentication Management
The centerpiece of 1.0 is server-side database support. With persistent storage, conversations and agents live beyond a single browser session. Switch from laptop to desktop without losing context. Build up institutional knowledge over time instead of starting from zero.
In the 0.x era, the most requested feature to be paired with server-side databases was user authentication management. Previously, we had integrated next-auth and Clerk as our authentication solutions. In response to demands for multi-user management, we have restructured the settings interface into a user panel, consolidating relevant user information within the new user interface.
This unlocks capabilities that were impossible in 0.x:
## LobeHub Cloud Beta Testing
- **Knowledge bases**: Store documents and reference them across conversations
- **Cross-device sync**: Pick up exactly where you left off on any device
- **Private agent marketplaces**: Share specialized agents within your team
- **Conversation history**: Search and revisit past discussions
LobeHub Cloud is our commercial version based on the open-source LobeHub, and all the features from version 1.0 are now live in LobeHub Cloud, which has entered beta testing. If youre interested, you can join our waitlist here. During the beta testing period, a limited number of access slots will be released daily for testing opportunities.
## User Management: From Single-Player to Multi-Player
Alongside the database, 1.0 introduces proper user authentication and management. We've integrated both next-auth and Clerk as authentication providers, giving you flexibility based on your security needs.
The settings area has been restructured into a dedicated user panel that brings identity, preferences, and access control into one place. This is essential infrastructure for teams. Multiple users can now share a LobeHub instance with proper access boundaries and account management.
Use the new panel to:
- Manage account settings and API keys in one place
- Configure authentication providers (next-auth or Clerk)
- Control workspace access for team members
- Switch between personal and team contexts
## LobeHub Cloud: Managed 1.0, Ready to Use
LobeHub Cloud is our hosted offering built on the 1.0 architecture. All the capabilities above—server-side persistence, user management, knowledge bases—are available now without any setup.
We've opened a beta waitlist with limited daily access. If you want to skip self-hosting and start using LobeHub 1.0 immediately, [join the waitlist here](https://lobehub.com).
## Improvements and fixes
- Migrated core storage layer from localStorage to PostgreSQL
- Added database migration system for seamless upgrades
- Implemented session management with secure token handling
- Refactored settings UI into dedicated user panel
- Added support for multiple authentication providers
- Improved initial load performance with server-side rendering
## Credits
Huge thanks to everyone who contributed to the 1.0 architecture overhaul. This release represents foundational work by the entire LobeHub team that will support the platform for years to come.
@@ -1,8 +1,8 @@
---
title: LobeHub 1.0新的架构与新的可能
title: LobeHub 1.0为持久化、多用户协作而生的新架构
description: >-
LobeHub 1.0 带来了服务端数据库、用户鉴权管理的全新架构与特性,开启了新的可能 。在此基础上, LobeHub Cloud 开启 Beta
版测试
LobeHub 1.0 引入服务端数据库支持和完善的用户管理体系,实现知识库、跨设备同步和团队协作能力。
LobeHub Cloud 同步开启 Beta 测试,内置全部新特性
tags:
- LobeHub
- 服务端数据库
@@ -10,18 +10,51 @@ tags:
- Beta 测试
---
# LobeHub 1.0新的架构与新的可能
# LobeHub 1.0为持久化、多用户协作而生的新架构
自从 3 月份宣布迈向 1.0 ,我们就开始着手全方面的升级。经过 2 个月的密集研发,我们很高兴地宣布 LobeHub 1.0 正式发布了!一起来看看我们的全新样貌吧~
从三月宣布迈向 1.0 开始,我们从底层彻底重构了 LobeHub。两个月后,1.0 正式到来。这不仅是增量更新,更是全新架构 —— 它实现了用户最迫切需要的核心能力。
## 服务端数据库支持
0.x 时代依赖浏览器存储。快速、简单,但受限。无法跨设备同步、无法构建知识库、无法与团队共享助手。每次会话都从零开始。LobeHub 1.0 改变了这一基础。
在 LobeHub 1.0 中,最大的特性是支持了服务端数据库。在 0.x 时代,由于缺乏服务端持久化存储,许多用户迫切需要的功能实现困难,或完全无法实现,例如知识库、跨端同步、私有助手市场等等。
## 服务端数据库:数据持久化、随时随地访问
## 用户鉴权管理
1.0 的核心是服务端数据库支持。有了持久化存储,对话和助手不再局限于单个浏览器会话。从笔记本切换到台式机,上下文无缝衔接。持续积累知识,而非每次重启。
0.x 时代,和服务端数据库搭配的呼声最高的特性就是用户鉴权管理。在此之前,我们已经接入了 next-auth 和 clerk 作为鉴权解决方案。并针对多用户管理的诉求,将设置界面重构为了用户面板,在新的用户面板中整合了相关的用户信息。
这解锁了 0.x 时代无法实现的能力:
## LobeHub Cloud 开启 Beta 测试
- **知识库**:存储文档并在多轮对话中引用
- **跨设备同步**:在任何设备上精准续接上次工作
- **私有助手市场**:在团队内共享专用助手
- **对话历史**:搜索和回顾过往讨论
LobeHub Cloud 是我们基于 LobeHub 开源版的商业化版本,上述 1.0 的功能在 LobeHub Cloud 中均已上线,目前已开启 Beta 测试。如果你感兴趣,可以在这里加入我们的 waitlist , Beta 测试期间每天都会发放体验名额。
## 用户管理:从单人使用到团队协作
伴随数据库升级,1.0 引入了完善的用户认证与管理。我们同时集成 next-auth 和 Clerk 作为认证提供商,让你根据安全需求灵活选择。
设置界面已重构为独立的用户面板,将身份、偏好和访问控制统一整合。这是团队协作的基础设施。多用户现在可以在同一 LobeHub 实例中工作,拥有清晰的权限边界和账户管理。
用户面板支持:
- 在同一处管理账户设置和 API 密钥
- 配置认证提供商(next-auth 或 Clerk
- 控制团队成员的工作空间访问权限
- 在个人和团队上下文间切换
## LobeHub Cloud:开箱即用的托管 1.0
LobeHub Cloud 是我们基于 1.0 架构打造的托管服务。上述全部能力 —— 服务端持久化、用户管理、知识库 —— 现已无需任何配置即可使用。
我们已开放 Beta 测试等待名单,每日发放有限体验名额。如果你想跳过自托管流程、立即使用 LobeHub 1.0[可在此加入等待名单](https://lobehub.com)。
## 体验优化与修复
- 核心存储层从 localStorage 迁移至 PostgreSQL
- 新增数据库迁移系统,确保平滑升级
- 实现带安全令牌处理的会话管理
- 重构设置界面为独立用户面板
- 新增多认证提供商支持
- 通过服务端渲染优化首屏加载性能
## 致谢
衷心感谢所有参与 1.0 架构重构的贡献者。本次发布凝聚了整个 LobeHub 团队的基础建设工作,将为平台未来数年的发展提供坚实支撑。

Some files were not shown because too many files have changed in this diff Show More