Compare commits

...

277 Commits

Author SHA1 Message Date
AmAzing- 32f135a5e5 Fix message edit send behavior 2026-04-23 13:20:20 +08:00
YuTengjing cad10007ef 📝 docs(skills): add sub-issue tree guide to linear skill (#14076) 2026-04-23 11:33:30 +08:00
René Wang 73860a9ffd 📝 docs: add April 20 weekly changelog (#14072) 2026-04-23 10:38:46 +08:00
Hardy 4696968edb 🐛 fix: add env var support for Coding Plan and OpenCode providers (#14064)
* 🐛 fix: add env var support for missing Coding Plan providers

Add zod schema and runtimeEnv mappings for BailianCodingPlan,
GLMCodingPlan, MinimaxCodingPlan, and VolcengineCodingPlan in llm.ts.
These were missing when the providers were added in #13203, causing
them to fall back to OPENAI_API_KEY instead of their own env vars.

* 🐛 fix: add env var support for OpenCode Zen and OpenCode CodingPlan providers

Add zod schema and runtimeEnv mappings in llm.ts for OpenCodeZen and
OpenCodeCodingPlan providers introduced in #13943. Without these,
getParamsFromPayload falls back to OPENAI_API_KEY.
2026-04-23 10:31:14 +08:00
Hardy 48760e353a feat: add OpenCode Zen and OpenCode Go providers (#13943)
*  feat: add OpenCode Zen and OpenCode Go providers

Add support for OpenCode Zen (dynamic model gateway) and OpenCode Go
(subscription-based coding plan) with full model definitions, runtime
implementations, and provider configurations.

- OpenCode Zen: curated models via single API key, dynamic model fetching
- OpenCode Go: coding models (GLM, Kimi, MiMo, Qwen, MiniMax)
- Both use @ai-sdk/openai-compatible runtime
- Go models include abilities, pricing, and extendParams settings

*  feat: add 35 preset models to OpenCode Zen provider

Populate OpenCode Zen with all non-deprecated models from models.dev API
including Anthropic (9), OpenAI (13), Google (2), Zhipu GLM (2), Alibaba
Qwen (2), Kimi (1), MiniMax (2), Nvidia (1), and OpenCode (1). Switch
from dynamic model fetching to static model list.

* ♻️ refactor: migrate OpenCode Zen/Go to RouterRuntime and align extendParams

Migrate both providers from openaiCompatibleFactory to createRouterRuntime
to match OpenCode's native multi-SDK architecture:

Zen (4 routers):
- anthropic for Claude, google for Gemini, openai+Responses for GPT-5.x,
  openai fallback for all others (GLM/Kimi/MiniMax/Qwen)

Go (2 routers):
- anthropic for MiniMax M2.5/M2.7, openai fallback for all others

Fix model-bank extendParams to match OpenCode variants() behavior:
- Remove extendParams from GLM/Kimi/MiniMax/BigPickle/Nemotron (variants return {})
- Change Qwen from enableReasoning+reasoningBudgetToken to reasoningEffort
- Change Go MiMo to reasoningEffort

* 🐛 fix: fix OpenCode Zen/Go Anthropic baseURL and remove Google router

- Add stripV1() to strip trailing /v1 from baseURL for Anthropic SDK
  since it auto-appends /v1/messages to the base URL
- Remove Google router from Zen - Gemini models fall to openai-compatible
  fallback as Zen Gateway does not support Google SDK format
- Keep user-configurable baseURL support while preventing /v1 duplication

* 🐛 fix: add missing package.json exports for opencode and stepfunCodingPlan

*  feat: limit default enabled models to latest versions for OpenCode Zen/Go

Zen: claude-opus-4-7, gemini-3.1-pro, gpt-5.4, glm-5.1,
     minimax-m2.5-free, nemotron-3-super-free, big-pickle
Go: glm-5.1, qwen3.6-plus, minimax-m2.7

* 🐛 fix: include opencodego in Coding Plan provider tag check

* ♻️ refactor: align model display names with official provider naming

Update Qwen3.6 Plus, Qwen3.5 Plus, and MiMo-V2 Omni display names
to use spaces instead of hyphens, matching the official provider naming
convention used in lobehub.

* ♻️ refactor: rename opencodego to opencodecodingplan for suffix consistency

Rename internal ID from opencodego → opencodecodingplan to align with
other Coding Plan providers. Display name remains "OpenCode Go".
This allows isCodingPlanProvider() suffix check to work without exceptions.

* 🐛 fix: remove broken stepfunCodingPlan export — file not on this branch

* ♻️ refactor: align MiMo-V2 Pro display name with official provider naming

* 🌐 i18n: add Chinese translations for OpenCode Coding Plan and Zen providers
2026-04-23 02:13:09 +08:00
Tsuki 70e7e441b2 🔨 chore: premerge Task detail page UI (#13653)
*  feat: add AgentTaskList component on agent welcome page (LOBE-6597)

- AgentTaskList with TaskListHeader, TaskItem, and styles
- Embedded in AgentWelcome below ToolAuthAlert
- Each task rendered as independent rounded card with status badge
- Status: green filled circle (Done), blue circle (In progress)
- Card width matches chat input (960px)
- i18n keys for taskList.title and taskList.viewAll
- Fix updateReview type to use TRPC-inferred type

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

*  feat: add Tasks page at /agent/:aid/tasks with route, breadcrumb, and view toggle (LOBE-6597)

- Register tasks route in both desktopRouter.config.tsx and .desktop.tsx
- Thin route page at src/routes/(main)/agent/tasks/index.tsx
- Feature components in src/features/AgentTasks/: page, breadcrumb, header with list/kanban toggle, full task list
- Wire up "View All Tasks" navigation from AgentTaskList welcome card
- Add i18n keys (taskList.activeTasks, taskList.breadcrumb.task) and generate translations via pnpm i18n

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

*  feat: add Task detail page at /agent/:aid/tasks/:taskId (LOBE-6597)

- Register :taskId child route in both desktopRouter configs
- TaskDetailPage with auto-save hint, breadcrumb, and scrollable content
- TaskDetailHeader: editable title (borderless Input), Run/Pause button, status/priority tags, delete
- TaskInstruction: click-to-edit Markdown with debounced auto-save
- TaskSubtasks: sub-issues list with status badges
- TaskActivities: timeline with topic/brief/comment icons
- TaskItem now navigates to detail page instead of just setting activeTaskId
- Add taskDetail.* i18n keys with generated translations

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

*  feat: add TaskModelConfig, TaskScheduleConfig, and refine Task detail UI (LOBE-6597)

Add model/provider selector and periodic execution config to Task detail page.
Refine TaskDetailHeader, TaskInstruction with auto-save and i18n support.

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

*  feat: refine Task detail UI with Linear-style design (LOBE-6597)

- Redesign SubTasks with collapsible header, progress circle, hover + click navigation
- Redesign Activities with agent avatar, comment input box, and Linear-style layout
- Add TaskParentBar showing parent task relationship with sibling navigation popover
- Add delete confirmation modal using App.useApp().modal.confirm
- Move ModelSelect to separate row below action bar
- Fix zustand selector recreation in ActivityItem
- Replace hardcoded colors with cssVar tokens

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

*  feat: add Properties panel, parent link hover, activity icon, and lifecycle save status (LOBE-6597)

- Add TaskProperties sidebar with collapsible status/priority dropdowns
- Parent bar: clickable parent link with hover, sibling navigation popover on progress
- Activity title: add BotMessageSquare icon
- Fix lifecycle actions not updating taskSaveStatus (saving/saved indicator)
- Filter status dropdown to only user-selectable states (backlog/completed/canceled)
- Add test task creation script for dev

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

*  feat: add recursive tree view for subtasks with Linear-style connecting lines (LOBE-6597)

- Add buildTaskTree utility to convert flat getTaskTree API response into nested tree
- Implement SubtaskTreeItem recursive component with CSS connecting lines (├─ and └─)
- Fetch full task tree via taskService.getTaskTree for nested subtask display
- Show loading spinner during tree fetch, fallback to flat list on error
- Remove padding-inline from AgentTaskList container

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

* 🐛 fix: address PR review — delete redirect, debounce cleanup, schedule resync (LOBE-6597)

- Redirect to task list after successful delete (P1)
- Clean up instruction debounce timer on unmount/task switch to prevent stale writes (P1)
- Resync TaskScheduleConfig local state when active task changes (P2)

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

* ♻️ refactor: use backend nested subtasks directly, remove buildTaskTree (LOBE-6597)

Backend now returns nested subtasks in task.detail (LOBE-6814).
Remove buildTaskTree utility, getTaskTree API call, and loading state.
Use TaskDetailSubtask from @lobechat/types instead of local interface.

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

*  perf: add optimistic update and save status for model config change (LOBE-6597)

updateTaskModelConfig now immediately reflects new model/provider in UI
via optimistic store dispatch, and tracks taskSaveStatus (saving/saved).

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

*  perf: skip redundant refreshTaskDetail on successful model config update (LOBE-6597)

Optimistic update is trusted on success — no need for full detail re-fetch.
Aligns with updateTask pattern. Refresh kept only in error path for revert.

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

*  feat: use backend author info for activities, fix AgentTaskList after AgentHome refactor (LOBE-6597)

- Activity: use act.author (TaskDetailActivityAuthor) from backend instead of agentMap lookup (LOBE-7013)
- AgentTaskList: fix agentId from useParams instead of useAgentStore.activeAgentId (was undefined)
- AgentHome: integrate AgentTaskList into new AgentHome layout (replaces old AgentWelcome)

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

*  feat: show participant avatars on task cards, use backend author for activities (LOBE-6597)

- TaskItem: display up to 3 participant avatars next to task title (LOBE-6805)
- Activity: use act.author from backend instead of agentMap lookup (LOBE-7013)
- AgentHome: integrate AgentTaskList into new AgentHome layout
- Revert AgentTaskList/TaskItem agentId back to useAgentStore (works correctly when mounted)

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

* ♻️ refactor: fix type safety, memoize participants filter, extract avatar styles (LOBE-6597)

- Use TaskParticipant type instead of `any` in filter/map
- Compute displayParticipants once with useMemo (was filtering twice per render)
- Move avatar overlap styles to CSS classes (was inline objects per render)

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

* 🔇 chore: hide kanban view toggle until implemented (LOBE-6597)

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

* ♻️ refactor: export TaskStatus/TaskPriority/TaskActivityType from @lobechat/types (LOBE-6597)

Replace hardcoded string/number types with shared type aliases:
- TaskStatus: 'backlog' | 'canceled' | 'completed' | 'failed' | 'paused' | 'running'
- TaskPriority: 0 | 1 | 2 | 3 | 4
- TaskActivityType: 'brief' | 'comment' | 'topic'

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

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

*  feat: add Daily Brief module to homepage (#13851)

*  feat: add Daily Brief module to homepage

Add a Daily Brief section below the chat input on the homepage that
displays unresolved briefs from the Agent Tasks system. Users can
resolve, comment, and provide feedback directly from the brief cards.

- Service: BriefService with listUnresolved, resolve, markRead, addComment
- Store: Independent Zustand store (src/store/brief/) with SWR data fetching
- Components: BriefCard, BriefCardActions (dynamic action buttons),
  BriefCardSummary (Markdown with expand/collapse), CommentInput (@lobehub/editor)
- Three action types: resolve (closes brief), comment (resolve with text),
  link (safe URL navigation with protocol validation)
- Fixed feedback button: adds task comment without resolving the brief
- Inline success state ("Feedback sent") with 1.5s auto-restore
- i18n: zh-CN + en-US translations
- Tests: 21 tests across service, store selectors, and components
- CLI: Register task and brief commands for local development

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

*  feat: add agent avatars to Daily Brief cards

Display stacked agent avatars next to brief card titles using the
new `agents` data from Arvin's enriched listUnresolved API (#13489).

- Add AgentAvatarInfo type and agents field to BriefItem
- Render overlapping circular avatars (20px, -6px overlap)
- Use cssVar.colorBgContainer for border (dark mode compatible)
- Extract avatar style to function to avoid inline object creation

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

* ♻️ refactor: clean up Daily Brief components

- Extract duplicate success state JSX into reusable SuccessTag component
- Remove redundant comments that describe what code does
- Use DEFAULT_AVATAR from @lobechat/const instead of hardcoded emoji

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

* 🐛 fix: address PR review feedback for Daily Brief

- Use cssVar.colorBgBase instead of hardcoded #fff for primary button
  text color (dark mode contrast fix)
- Add submitting state to CommentInput to prevent duplicate submissions
  (disable buttons + show loading during async submit)

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

* 🌐 chore: generate i18n translations for Daily Brief

Run pnpm i18n to generate translations for all 18 locales.

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

* ♻️ refactor: use shared BriefType from @lobechat/types

Export BriefType union from packages/types and use it in
BRIEF_TYPE_COLOR and BRIEF_TYPE_ICON records for compile-time
key validation. Adding a new brief type now requires updating
the shared type, and TypeScript will flag missing mappings.

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

* style: update

* style: update

* style: update

---------

Co-authored-by: Tsuki <976499226@qq.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: update

* style: update

* style: update

* style: update

* fix: stopPropagation

* fix: i18n

* 🐛 fix: wire comment inputs to editor instance so Send actually submits

CommentInput in AgentTasks and DailyBrief used antd TextArea inside
@lobehub/editor's ChatInput while reading content via
editor.getDocument('markdown'). The TextArea was never connected to the
editor instance, so getDocument always returned empty and handleSubmit
short-circuited silently — Send appeared to do nothing (no network
request fired).

Replace the TextArea with <Editor editor={editor} type="text"
variant="chat" /> so useEditor() actually drives the editable surface.
Keep plain-text behavior via markdownOption={false} +
enablePasteMarkdown={false}, and bind Cmd/Ctrl+Enter submit via
onPressEnter.

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

* 🐛 fix: use participant.title after TaskParticipant schema rename (#13877)

PR #13877 renamed TaskParticipant.name → .title and added
.backgroundColor. Our branch's UI code (AgentAvatars, listViewOptions,
TaskList group header, Breadcrumb) was already written against the new
schema, but TaskProperties still read firstParticipant?.name — update
the last remaining call site so the type matches post-rebase.

backgroundColor is already plumbed through everywhere it applies within
#13877's scope; TaskActivities' TaskDetailActivityAuthor is a separate
type untouched by the PR and kept as-is.

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

* 🐛 fix: resolve type-check errors exposed after canary rebase

canary upgraded react-i18next to a version with typed i18n keys and
tightened @lobehub/editor's SendButton + IEditor APIs. Rebase pulled
these in, surfacing latent type errors in LOBE-6597 code.

- CommentInput: use editor.cleanDocument() (IEditor's actual API;
  clearContent never existed).
- TaskActivities / TaskLatestActivity / TaskTriggerTag: type t as
  TFunction<'chat'> so typed i18n accepts the known-literal keys used
  inside module-level helpers.
- TaskPriorityTag / TaskStatusTag / listViewOptions: add
  defaultValue: '' to dynamic-key t() calls (template literals and
  Record lookups) to match the broad-key i18n overload.
- BriefCardActions: swap unusable <SendButton> (no children, no
  iconPlacement) for <Button>; add defaultValue to the dynamic
  brief-action key lookup; drop stale @ts-ignore.
- DailyBrief/CommentInput: drop unsupported children on SendButton;
  keep label via title attribute.
- Recents/Item: type TYPE_ICON_MAP as Partial<Record<...>> so 'task'
  (rendered via TaskStatusIcon elsewhere) is a safe absent key.
- brief/slices/list/action: cast briefService.listUnresolved() result
  back to BriefItem[] (TRPC serialization widens BriefType to string).
- AgentTasks/TasksHeader: delete dead file — no importers and its
  ./style module was removed by an earlier refactor.

Also ran pnpm install to materialize the newly-extracted
@lobechat/agent-gateway-client workspace package (canary #13866),
clearing ~7 "cannot find module" errors.

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

* ♻️ refactor(builtin-tool-task): polish task tool paths (#13869)

*  feat: navigate to task detail when clicking brief card header

Clicking the header row of a Daily Brief card (icon + title + time +
agent avatars) now jumps straight to the associated task, using the
brief's task-tree agent (with activeAgent / inbox as fallback).

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

*  feat: show parent task ids as clickable breadcrumb trail

Walk the cached parent chain from taskDetailMap and insert each ancestor's
identifier as a link between the "任务" entry and the current task name in
the task detail breadcrumb.

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

*  feat: add cross-agent /tasks page with View All Tasks on Daily Brief

- Register `/tasks` route in desktop (web + Electron) and mobile router configs
- `useFetchTaskList` supports `allAgents` mode via options object API to fetch
  tasks without agent filter; backend already supports optional assigneeAgentId
- `Breadcrumb` accepts optional `agentId`, renders "All tasks" crumb when absent
- `AgentTaskItem` navigation uses `task.assigneeAgentId` so clicks work from
  the cross-agent page (falls back to `activeAgentId` for unassigned tasks)
- Extract `useScenarioEnabledTools` hook to share layout effect between
  `/tasks/_layout` and `/agent/:aid/tasks/_layout`

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

* ♻️ refactor: use assigneeAgentId for task avatar instead of participants array

Replace AgentAvatars (took participants[]) with AssigneeAvatar (takes agentId,
resolves meta from agent store). This correctly represents that a task is
assigned to a single agent via assigneeAgentId/detail.agentId.

- New AssigneeAvatar component reads agent meta from agent store by ID
- TaskProperties reads activeTaskAgentId from task detail store
- listViewOptions uses task.assigneeAgentId directly for groupBy/sort
- Extract shared isInboxAgentId helper to eliminate 4x inline duplication
- Group headers resolve agent title at render time via AssigneeLabel component

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

* 🐛 fix: enable vertical scrolling on cross-agent tasks page

Add overflowY and flex to WideScreenContainer wrapper so the task list
can scroll when content exceeds viewport height.

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

*  feat: add re-assign task agent with popover selector

- Add AssigneeAgentSelector component with Popover agent list
- Extract useAgentDisplayMeta hook for consistent agent name/avatar resolution
- Fix optimistic update mapping assigneeAgentId → agentId in task store
- Disable reassignment for running tasks with tooltip hint
- Integrate selector into task list and task detail property panel

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

*  feat: reuse BriefCard in task detail activities & fix raw-id navigation

Render brief-type activities as full BriefCard (same as homepage) instead of
plain tree rows. Decouple BriefCardActions from useBriefStore for actions
lookup so it can be reused across pages. Fix infinite loading when navigating
to task detail via raw DB id (task_xxx) by storing detail under both the
identifier and the raw id key in taskDetailMap.

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

*  feat: add TopicCard component for task detail activities

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

*  feat: allow re-running completed tasks with dedicated button

Completed tasks now show a "Re-run" button (with rotate icon) instead of
hiding the action. The backend already supported this — only the frontend
selector gate needed updating.

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

*  feat: add create task modal with markdown editor

Add a "+" button on the tasks list page that opens a Linear-style modal
for manually creating tasks. The modal features a title input, a markdown
editor (EditorCanvas), and a bottom toolbar with priority and assignee
selectors. Existing tag components (TaskStatusTag, TaskPriorityTag,
AssigneeAgentSelector) are extended with an `onChange` controlled mode
so they can be used in creation context where no task exists yet.

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

* 🐛 fix: suppress spurious updateTask on Task Detail page load

EditorDataMode was missing the contentChangeLockRef pattern that
DocumentIdMode already uses, causing Lexical's registerUpdateListener
to treat programmatic content hydration as a user edit and fire
onContentChange → updateTask on every page visit.

- Add contentChangeLockRef + lockIdRef staleness guard
- Extract loadContentWithLock to deduplicate lock/load/unlock logic
- Pass contentChangeLockRef to InternalEditor
- Remove unreachable dead code in loadEditorContent

Closes LOBE-7362

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

*  feat: task detail comment CRUD and various UX improvements

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

* 🐛 fix: move canceled status group to the end of task list

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

* 💄 style: polish task detail layout, title, and run button

- Title switched to auto-sizing TextArea so long names wrap (like Linear)
- Reduce title font-size from 32px to 24px and tighten paddings
- Make "运行任务" button small-sized to match the denser header
- Add 120px bottom padding for end-of-content scroll breathing room
- Default EditorCanvas paddingBottom trimmed from 64 to 32

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

* 💄 style: refine task assignee, priority, and comment input

- Assignee block uses filled variant in dark mode for better contrast
- Urgent priority (level 1) renders in orange for quick scanning
- Comment input keeps SendButton slot reserved to prevent layout shift

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

*  feat: task detail — inline subtasks, automation mode, chronological activity

- Inline subtask creation under a task via CreateTaskInlineEntry
  (parentTaskId/autoFocus/onCollapse/placeholder), refreshes parent on create
- Track agent-created tasks via createdByAgentId through service, router,
  types, and the builtin task executor
- Replace scheduler Segmented-only UI with an Enable switch + heartbeat/
  schedule mode; persist via automationMode on the task
- Sort detail activities oldest → newest for a natural timeline reading
- Reducer patches nested subtask entries on updateTaskDetail so in-place
  edits reflect in the parent's subtask tree

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

* 💄 style: render activate-tool chips as rounded pills

Switch inspector tool chips from monospace code tags to filled rounded
pills with ellipsis overflow, making multi-tool rows scan better in tight
headers.

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

* 🐛 fix: keep finished tool call out of loading state while siblings run

The message-level isAssistantMessageBusy flag stays true while sibling
tool calls are still running. Without guarding on this tool's own
result, a finished tool would flip back to "loading". Now a tool that
has a real result or error is never shown as calling.

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

* 💄 style: use small Segmented in schedule config popover

Keeps the automation mode switcher visually aligned with the denser
popover controls.

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

*  feat: agent profile hover card on task activity author

- Extract shared AgentProfileCard + unified AgentProfilePopup (click / hover)
  with lazy agent fetch; move out of group sidebar path.
- Wire activity author avatar + name to a hover card; brighten title on hover;
  keep a small "agent" tag on the author row.
- Show inline skeletons (description + footer stats) while loading.
- Enrich subtask payload with assignee agent info for cleaner UI.

*  feat: open task topic chat in side drawer

Click a topic row in the task detail activities to open a right-side drawer
showing the topic's full chat history. Messages stream in live via the existing
agent gateway pipeline (gateway events land in chatStore.dbMessagesMap keyed by
the topic context), so a running topic refreshes its drawer in real time without
a dedicated subscription.

Reuses the Conversation feature (ConversationProvider + ChatList) with an
isolated context (agentId + topicId + isolatedTopic), so the drawer never
touches the global active topic and multiple panels coexist cleanly.

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

* 💄 style: outline activate-tool chip with subtle border

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

*  feat: show topic handoff summary on activity card

Pull `handoff.summary` through the task service into TaskDetailActivity and
render it under the title in TopicCard so completed topics surface what was
accomplished without opening the drawer.

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

* 🎸 chore: gate agent task feature behind agent_task flag

Hide every client-side entry point to the Agent Task feature when the
`agent_task` flag (default `isDev`, off in prod) is disabled:

- Sidebar: task tab in the agent sidebar nav
- Routes: `/agent/:aid/tasks/*` and `/tasks/*` layouts redirect to `/` when
  the flag is off (mobile router reuses the same layout)
- Home Recents: filter out `type='task'` items in both the list and the
  "all recents" drawer
- Daily Brief: skip fetch + hide the entire panel (all briefs link to tasks)

Backend TRPC / lifecycle stays on — the feature is already live for CLI
usage. Flag name mirrors `agent_onboarding` for consistency.

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

* 🐛 fix: prioritize includeTriggers in topic queries

* 🐛 fix: normalize task detail activity payloads

*  feat: add Kanban board view for task list with drag-and-drop

LOBE-7493

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

* 💄 style: shorten schedule tag labels & fix time width in task cards

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

* update i18n

* 💄 style: hide task tool from user selectors

* 💄 style: hide task skill from user selectors

---------

Co-authored-by: canisminor1990 <i@canisminor.cc>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2026-04-23 02:10:45 +08:00
Innei 5196203414 ♻️ refactor: replace antd Modal with base-ui Modal in FileEditor (#14054)
♻️ refactor: replace antd Modal with imperative base-ui createModal

Replace the declarative antd Modal in AttachKnowledgeModal with imperative
createModal from @lobehub/ui/base-ui. The antd Modal's event handling
conflicted with the three-dot DropdownMenu in the file list, causing the
menu to be unclickable in Group Chat context.

Closes #12389
2026-04-23 01:36:11 +08:00
Innei 5c2fe6c579 🐛 fix(onboarding): unify footer visibility behind AGENT_ONBOARDING_ENABLED (#14065)
🐛 fix(onboarding): show mode switch and skip footer based solely on AGENT_ONBOARDING_ENABLED

Remove route-based conditional so the footer visibility is controlled
entirely by the AGENT_ONBOARDING_ENABLED flag.
2026-04-23 01:17:43 +08:00
Arvin Xu 042987fe34 🐛 fix(agent-runtime): unwrap underlying PG error in formatErrorEventData (#14038)
* 🐛 fix(agent-runtime): unwrap underlying PG error in formatErrorEventData

Drizzle wraps driver errors as "Failed query: insert into ..." and buries
the real PostgreSQL diagnostic fields (code, severity, detail, constraint,
column, table) in `.cause`. `formatErrorEventData` in RuntimeExecutors only
read the outer `.message`, so the agent-gateway dashboard saw nothing but
the SQL text — no way to bucket errors by SQLSTATE or tell apart a UTF-8
validation failure from a unique-constraint hit from a row-too-big.

Add a `pgError` util that walks `.cause` up to 5 layers, duck-types real
PG errors via `code` + a known `severity`, and exposes
`{ formatPgError, pgErrorType, unwrapPgError }`. `formatErrorEventData`
now invokes the unwrap as a last-step enrichment — only when no typed
errorType was identified — so typed errors like `ConversationParentMissing`
keep their clean business messages.

After this, the dashboard gets:
  error:     PG 22021 · ERROR · invalid byte sequence ... · table=message_plugins · column=state
  errorType: pg_22021
instead of:
  error:     Failed query: insert into "message_plugins" ...
  errorType: Error

Related: LOBE-7158, LOBE-7334

* 🐛 fix(agent-runtime): unwrap PG diagnostics for raw driver errors regardless of error.name

Review feedback on the prior commit: the enrichment branch only ran when
errorType was missing or exactly 'Error', so raw top-level driver errors —
`PostgresError` (postgres-js), `DatabaseError` (node-postgres), any
provider-specific subclass — kept their driver class name as errorType
and never reached the pg_<sqlstate> bucket. This defeated the new
classification for the exact case it was meant to catch: a PG error
surfacing directly from the driver without a Drizzle wrapper.

Fix: track whether `errorType` came from a business-typed field on the
error payload (step 1 — e.g. `ConversationParentMissing`) vs. from
`error.name` (step 3 — a driver class name). Only skip PG unwrap for
business-typed errors. Driver-named errors now fall through to unwrap
and emit `pg_<sqlstate>` when PG info is identifiable.

Also extract `formatErrorEventData` out of RuntimeExecutors.ts into its
own file so it can be unit-tested directly. The surrounding
RuntimeExecutors module pulls in workspace packages (`@lobechat/markdown-patch`,
`@lobechat/agent-gateway-client`, etc.) that don't resolve in the test
environment, blocking any test that imports from it.

Test coverage added (10 cases): top-level PostgresError class, plain
DatabaseError-shaped object, Drizzle .cause unwrap, ConversationParentMissing
preservation, custom errorType preservation, Node ENOTFOUND rejection,
null/non-object fallbacks, plain-string inputs, payload-with-only-message.
2026-04-23 00:46:01 +08:00
Innei f00d95f4a6 🐛 fix(desktop): add Linux icon configuration to electron-builder (#14042)
The Linux target was missing the icon field, causing the .deb package
to show no application icon on Ubuntu and other Linux distributions.

Closes #9785
2026-04-23 00:34:20 +08:00
Innei ed6330362c 🐛 fix(conversation): pin user message to viewport top & fold long user messages (#14056)
* 🐛 fix(conversation): pin user message to viewport top after spacer settles

Observing the spacer DOM via ResizeObserver lets us re-fire scrollToIndex
once virtua finishes measuring it and scrollSize actually expands, so the
sent user message lands flush against the viewport top instead of
trailing below by the spacer growth delta. Also drop the height
transition on mount/grow so scrollSize jumps in a single frame; only the
collapse-to-zero (unmount) still animates.

* 🐛 fix(vite): detach spawn for debug proxy so dev server isn't blocked

Swap execFile for a detached spawn with stdio ignored and unref, so the
opened browser process no longer keeps the Vite dev process alive. Falls
back to treating a 200ms "no error" window as success, and routes
diagnostics through the Vite logger instead of swallowing them.

*  feat(conversation): fold long user messages so AI response stays visible

When a very long user message is pinned to the viewport top after send,
it can eat the entire viewport and leave no room for the AI reply.
Wrap the user text body in a CollapsibleContent that clamps content
past min(280px, 35vh) with a gradient mask and a Show more / Show less
toggle. Attachments, images and page selections stay fully visible.

* ♻️ refactor(conversation): scope spacer observer to this list via ref callback

ConversationProvider supports multiple conversation lists mounted at the
same time, so a document-wide querySelector would attach to whichever
spacer the DOM hands out first — possibly another panel's — and drive
spacerLayoutVersion from unrelated layout ticks. Switch to a ref
callback returned from useConversationSpacer and bound to the spacer div
rendered by the same VirtualizedList, guaranteeing the observer tracks
this instance's own spacer.

* 🐛 fix(conversation): cancel queued pin retries when user scrolls up

Clearing pendingScrollIndexRef alone wasn't enough — the retry wave fires
at 0/32/96ms, so if the user scrolled up between send and 96ms the
already-queued timers would still call scrollToIndex and yank the
viewport back down, contradicting the "don't fight user intent" rule.
Also invoke clearPendingPins in the same effect so the in-flight retry
window is cancelled along with the pending index.
2026-04-22 23:59:43 +08:00
YuTengjing 17834d41c3 🐛 fix(route-log): record image/video generation triggers (#14048) 2026-04-22 23:48:59 +08:00
Innei 5e9546c537 🐛 fix(page-editor): use remoteServerUrl for copy link on desktop (#14057)
Fix LOBE-7356 — PageEditor handleCopyLink used window.location.origin which resolves to app://renderer on desktop. Now uses electronSyncSelectors.remoteServerUrl on desktop, consistent with existing pattern in global.ts and Topic dropdown.
2026-04-22 23:40:25 +08:00
Innei 25e4b3e33b 🐛 fix(build): enable Rolldown strictExecutionOrder for production builds (#14058)
Made-with: Cursor
2026-04-22 23:14:11 +08:00
Innei 82ba3706a7 feat(desktop): screen capture overlay, Quick Chat tray, and upload pipeline improvements (#13818)
* feat: add screen capture functionality with overlay support

- Implemented ScreenCaptureManager to handle screen capture sessions.
- Added ScreenCaptureCtr for IPC methods related to screen capture.
- Created overlay.html and ScreenCaptureOverlay component for user interaction.
- Integrated window enumeration and capture logic using node-screenshots and get-windows.
- Updated menu options to include screen capture actions.
- Enhanced RendererUrlManager to support overlay routing.
- Introduced drag selection for capturing specific screen areas.
- Added necessary types and events for screen capture in electron-client-ipc.

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

*  feat(desktop): refine screen capture overlay flow

*  feat(desktop): refine screen capture overlay flow

*  feat(desktop): optimize screen capture overlay flow

* Delete apps/desktop/mockup/screen-capture-overlay.html

*  feat(desktop): open mini toolbar via double Option

* 🐛 fix(desktop): separate quick composer hotkey

* 💄 fix(desktop): remove stale quick composer accelerator

* 🐛 fix(desktop): stabilize double option monitor

* 🐛 fix(desktop): read hardware option key state

* 🐛 fix(desktop): standardize path imports and improve error handling

- Replaced `join` imports with `path` imports for consistency across files.
- Enhanced error handling in various modules to include error causes for better debugging.
- Updated test files to reflect changes in variable naming and mock implementations.

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

* 🔥 chore(hotkey): drop orphan renderer quickComposer i18n entries

The `quickComposer` hotkey is registered only on the Electron side
(DESKTOP_GLOBAL_SHORTCUT_DEFAULTS + BrowserWindowsCtr.openQuickComposer);
the renderer never referenced these i18n keys, so the entries were dead.
`desktop.quickComposer` covers the app-level trigger.

* ️ perf(screen-capture): parallelize overlay upload with route navigation

Overlay submit used to await screenshot upload before router.push,
blocking the main window for several seconds when the user was on an
unrelated page (e.g. /settings). Now we navigate immediately and run
upload in a background IIFE; MessageFromUrl waits on a new
`uploadStatus` field before calling sendMessage, so the chat page
mount and the upload proceed in parallel.

- Add `uploadStatus: 'uploading' | 'ready' | 'failed'` to
  PendingOverlayDispatch; canConsumePendingOverlayDispatch blocks
  while `'uploading'`.
- Store gains `markDispatchUploadComplete`; on failure it clears
  screenshotFileNames so the prompt still delivers.
- Dispatcher drops stale prev search params on push to prevent
  MessageFromUrl's message-param effect from double-firing.

* ️ perf(screen-capture): pre-upload captures in overlay preview + per-thumbnail status

Move uploads from post-submit to preview time, bypassing dataUrl round-trips:

- Main process assigns captureId at preview time and ships the PNG bytes
  as ArrayBuffer to the main renderer via `overlayUploadRequest`.
- Main renderer uploads through a dedicated pool (uploadWithProgress,
  no chatUploadFileList pollution); reports status back to the overlay
  through `overlayCaptureUploadStatus`.
- Overlay thumbnails render a spinner / error badge based on status;
  the send button stays grey until every capture resolves to `ready`.
- Submit now carries only captureIds; MessageFromUrl awaits the pool
  promises before sendMessage, removing the second upload pass.
- Carry overlay-selected modelId/provider into the agent config so the
  first message actually uses the user-chosen model (fixes the bug where
  switching the model on the overlay had no effect).

* update

*  feat(popup): add Quick Chat tray entry backed by Inbox agent

Tray menu now exposes a "Quick Chat" action that opens (or focuses)
a single-instance popup window at `/popup/agent/inbox`. Each fresh
open starts with no active topic; the first message creates one
through the normal agent flow.

- New `PopupAgentQuickPage` resolves the inbox slug via
  `builtinAgentSelectors.inboxAgentId` so `activeAgentId` points at
  the real entity in `agentMap` (fixes the stuck-loading / skeleton
  state from using the literal `'inbox'` slug).
- `BrowserManager.openQuickChatPopup` wraps
  `createMultiInstanceWindow` with a fixed `topicPopup_quick_inbox`
  uniqueId so repeat clicks focus rather than spawn.
- Wire the action into macOS / Windows / Linux tray menus and add
  the `tray.quickChat` i18n key.

* Add quick chat shortcut and desktop hotkey support

*  feat(screen-capture): enhance window enumeration with scale factor support

- Updated `enumerateWindows` to accept an optional `displayScaleFactor` parameter for improved window geometry normalization on high-DPI displays.
- Refactored `normalizeWindowBounds` to handle scaling based on the provided scale factor, ensuring accurate window dimensions across different platforms.
- Adjusted tests in `WindowSourceService.test.ts` to validate the new scaling behavior for both Windows and macOS environments.
- Minor adjustments in `ScreenCaptureManager` to accommodate the updated window enumeration logic.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-22 22:00:30 +08:00
Rdmclin2 993f3f29ea 🐛 fix: slack webhook error (#14052)
* chore: add slack error docs

* chore: universal merge config and default schema settings

* fix: setting save lost

* chore: remove legacy webhook
2026-04-22 21:19:14 +08:00
Arvin Xu 2a3667493f feat(git-status): one-click pull/push from branch chip (#14041)
*  feat(git-status): one-click pull/push from branch chip

Split the ahead/behind indicator out of the BranchSwitcher trigger so
↓N / ↑N become standalone action chips: clicking ↓ runs `git pull
--ff-only`, clicking ↑ runs `git push`. Each chip swaps to a spinning
LoaderIcon while the operation is in flight and refreshes branch /
working-tree / ahead-behind state on success.

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

* ♻️ refactor(electron-ipc): extract Git IPC types into dedicated git.ts

Move GitBranchInfo / GitLinkedPullRequest(Result) / GitBranchListItem /
GitWorkingTree(Status|Files) / GitCheckoutResult / GitPullResult /
GitPushResult / GitAheadBehind out of system.ts into a sibling git.ts
so the system surface stays focused on system/window/theme types.

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

* 🐛 fix(git-status): push chip failing under push.default=simple

Use `git push -u origin HEAD` instead of bare `git push` so the one-click
push action works on branches whose upstream name differs from the local
name (the common `git checkout -b feat/x origin/canary` workflow). Bare
`git push` refuses in that case under the default simple policy.

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

* 🐛 fix(git-status): push tooltip lying about target ref

Push chip was reusing the pull upstream in its tooltip, which is wrong
when local branch name differs from upstream (e.g. feat/x tracking
origin/canary) — the push actually goes to origin/<local-name> per
our `git push -u origin HEAD`, not to the upstream.

Compute a separate `pushTarget` (`origin/<current-branch>`) and
`pushTargetExists` flag in getGitAheadBehind, and switch the push
tooltip to use that. When the target doesn't exist yet (one-click
creates a new remote branch) show a "(new branch)" variant so the
user knows what the click will do.

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

* 💄 style(git-status): ring spinner + clearer create-branch tooltip

- Swap the lucide LoaderIcon (with hand-rolled CSS spin) for the shared
  RingLoadingIcon used in Topic items, so the in-flight pull/push chip
  matches the rest of the app's spinner style.
- Reword the new-branch push tooltip from "push N commits to X (new
  branch)" to "Click to create branch X" — the count is misleading when
  the remote doesn't exist yet (the action is creating, not catching
  up), and the shorter copy reads cleaner.

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

* Simplify comments in pushGitBranch method

Removed detailed comments about git push behavior.

* 🐛 fix(git-status): serialize pull/push on diverged branches

Block the opposite sync action while a git sync is running — both chips
go disabled whenever pulling or pushing is true. Previously on a
diverged branch (ahead > 0 and behind > 0) a user could start pull and
still click push before the first finished, launching concurrent git
operations against the same worktree and producing lock / non-FF errors
plus confusing double toasts for a single intent.

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

*  feat(git-status): piggyback best-effort fetch on ahead/behind lookup

Problem: ahead/behind was computed purely against locally-cached refs, so
commits pushed to origin elsewhere (GitHub web UI, another machine) never
surfaced as ↓N until the user ran `git fetch` in a terminal.

Fix: run `git fetch --no-tags --quiet origin` at the start of
getGitAheadBehind with a 10s timeout; ignore failures and fall through
to compute against whatever refs we have. SWR's revalidateOnFocus
already re-invokes this IPC, so the fetch happens on window re-focus for
free — no new UI and no interval polling.

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-22 20:53:35 +08:00
Innei 9c5831ac54 🐛 fix(vite): exclude .html from code-inspector to fix Vite 8 bundledDev (#14053)
* 🐛 fix(vite): exclude .html from code-inspector to fix bundledDev

* 🔥 chore: remove @code-inspector/vite patch, fixed via exclude config
2026-04-22 20:43:24 +08:00
Innei 31d76ccb90 ⬆️ chore: upgrade Vite to 8.0.0 (#12720)
* ⬆️ chore(vite): migrate SPA build pipeline to Vite 8

* 🔧 chore(vite): patch inspector tooling and stabilize rolldown output

* 🐛 fix(vite): apply Vite 8 follow-up fixes and dev proxy polish

* 🩹 chore(vite): drop oversized code-inspector core patch

* 🐛 fix(desktop): support vite 8 electron build

* 🐛 fix(desktop): declare mac permissions types ambiently

* 🐛 fix(desktop): externalize mac permissions in main build
2026-04-22 19:59:38 +08:00
Innei 9a03c182da ♻️ refactor(desktop): increase recent working directories from 5 to 20 with scroll container (#14036)
* ♻️ refactor(desktop): increase recent working directories from 5 to 20 with scroll container

* 🎨 style(branch-switcher): compact dropdown, immersive search, aligned icons

- Stop keydown propagation on inputs to bypass Base UI typeahead navigation
- Switch search input to borderless variant with bottom divider
- Align search prefix icon with list item icons at 12px
- Tighten item padding, line-height and meta spacing
- Match create-branch item radius to popup via calc(borderRadius - 4px)
2026-04-22 17:14:06 +08:00
YuTengjing 9d41c8b71c 🐛 fix(mobile): correct session list skeleton row layout (#14040) 2026-04-22 17:04:51 +08:00
YuTengjing 16f2b97de2 feat: add gpt-image-2 to LobeHub-hosted card (#14039) 2026-04-22 16:57:31 +08:00
Arvin Xu 6d339d6a64 🐛 fix(agent-runtime): sanitize invalid tool_call arguments to unbreak strict providers (#14033)
* 🐛 fix(agent-runtime): sanitize invalid tool_call arguments to prevent history poisoning

When a model emits malformed JSON as tool_calls[].arguments (e.g. Qwen
producing `{, "description": ...}`), the raw string was persisted to
`messages.tools[].arguments` and replayed verbatim on every subsequent
turn. Strict providers (NVIDIA NIM) validate the full history and 400
the whole request, terminating the op and wasting all accumulated tokens.

Add a shared `sanitizeToolCallArguments` helper in @lobechat/utils and
wire it in at three layers so both new captures and already-poisoned DB
history are safe:

- Server entry (RuntimeExecutors onToolsCalling) — mirrors the frontend's
  `internal_transformToolCalls` pattern; prevents new poisoning.
- Outbound context build (ToolCallProcessor) — last line of defense for
  historical messages that were persisted before this fix.
- Agent-runtime core (call_tools_batch normalization) — covers the
  old-format ToolsCalling[] path.

Behavior: valid JSON passes through unchanged (prompt cache stable);
partial-json recovers truncated streams; unrecoverable payloads fall
back to "{}" so the tool_call structure survives and the model can
replan on the next turn.

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

* 🐛 fix(agent-runtime): preserve INVALID_JSON_ARGUMENTS feedback when sanitizing

Sanitizing `tool_calls[].arguments` at capture (onToolsCalling) was too
early — the normalized "{}" reached `BuiltinToolsExecutor.execute` and
bypassed the `INVALID_JSON_ARGUMENTS` branch, so the model got a generic
"missing required field" error instead of the precise "your JSON syntax
was broken, fix it" feedback. That regressed the self-reflection signal.

Move sanitization to the persist boundaries only:
- DB write via `messageModel.update({tools: ...})`
- `state.messages` push for the assistant message's `tool_calls`

The execution path keeps the raw `arguments` string so the executor can
still emit its `INVALID_JSON_ARGUMENTS` tool-result with the original
malformed payload echoed back — exactly the frontend-symmetric self-
reflection flow.

Add a regression test pinning the LOBE-7761 Qwen shape so future changes
can't silently drop the feedback again.

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

* 🐛 fix(agent-runtime): drop sanitize from runtime normalization to avoid undeclared @lobechat/utils dep

Review flagged that `runtime.ts` imported `sanitizeToolCallArguments` from
`@lobechat/utils` while `agent-runtime/package.json` doesn't list utils as
a runtime dependency — in strict/hermetic installs this resolves to
MODULE_NOT_FOUND before the runtime can start.

Rather than add a new dep just for a belt-and-suspenders path, drop the
sanitize on the old-format `call_tools_batch` normalization. The actual
LOBE-7761 bug is server-side history poisoning; that's fully covered by:

- RuntimeExecutors persist-boundary sanitize (DB write + state.messages)
- context-engine ToolCallProcessor outbound sanitize (handles any DB
  history that was persisted before this fix)

Old-format agents in agent-runtime don't persist or replay to providers
on their own — sanitization is the consuming application's
responsibility and can live closer to its persistence layer.

Drops the dep-cycle-free path.
Related LOBE-7761
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): log tool_call parse errors in Anthropic adapter

The assistant→Anthropic conversion was swallowing `JSON.parse` errors
silently and falling back to empty `input: {}`. Combined with the
LOBE-7761 fix, bad arguments should always be sanitized upstream in
context-engine, so hitting this catch means something bypassed the
defense and we're about to send a tool_use with empty input to Claude.
That's worth knowing about.

Match the `console.error('parse tool call arguments error:', ...)`
pattern already used in openaiCompatibleFactory so logs are greppable.

Related LOBE-7761
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-22 16:09:26 +08:00
LiJian 9e4bcf88c6 🐛 fix: add the inbox agentid Sync to resume the /agnet/inbox/message=xxx link (#14035)
* fix: add the inbox agentid Sync

* fix: should have the fallback

* fix: update the test
2026-04-22 15:20:08 +08:00
AmAzing- b8cd21a257 chore: add Twitter to recommended skills (#14037) 2026-04-22 15:08:38 +08:00
YuTengjing b4de72b032 feat(mobile): full settings menu and responsive profile layout (#14019) 2026-04-22 15:08:34 +08:00
Arvin Xu e963c640b9 🎨 style(claude-code): tool inspector polish + unstick Read-on-image spinner (#14034)
* 💄 style(claude-code): prefix Agent inspector with "Agent:" and drop chip 60% cap

Row visibly reads as a subagent dispatch, not a generic tool; chip no longer
ellipsizes when there is room to the right.

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

* 🐛 fix(heterogeneous-agents): unstick Read tool spinner on image results (LOBE-7338)

CC's `Read` on images returns a `tool_result` whose `content` is an `image`
block (base64). The generic array mapper had no branch for it so resultContent
collapsed to '' and the UI's StatusIndicator stuck on the spinner. Emit a
minimal `[Image: <media_type>]` placeholder so the tool ends in completed
state. Richer image echo (thumbnails) is tracked separately and needs
structured ToolResultData.

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

* 💄 style(claude-code): place "Agent:" prefix before the icon

Order is now `Agent: <icon> <subagent_type>` instead of `<icon> Agent: <subagent_type>` so the contextual label leads, the bot icon sits between as a visual separator, and the subagent name closes the row.

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-22 14:33:54 +08:00
Arvin Xu 1f61e965a6 🎨 style(claude-code): tool inspectors + heterogeneous-agent follow-ups (#14030)
*  feat(claude-code): render ScheduleWakeup / TaskOutput / TaskStop in inspector

CC emits three tool calls we were previously rendering as raw JSON:
`ScheduleWakeup` (self-paced /loop), `TaskOutput` (read from background
task), `TaskStop` (terminate background task). Add dedicated inspectors
and register them alongside the existing CC tool set.

`TaskStop` accepts both `task_id` and the legacy `shell_id` field name
since older CC builds still emit the latter.

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

* 🐛 fix(chat-topic): stop completed topics from leaking past the sidebar filter

Two sibling components in each chat-topic sidebar were both calling
`useFetchTopics`, but with different args: the outer `Topic` passed the
preference-driven `excludeStatuses: ['completed']` filter while the
inner `List` / `TopicListContent` called it bare. Since `excludeStatuses`
is part of the SWR key, both calls fired independent requests whose
`onData` handlers wrote back to the same `topicDataMap[containerKey]`
slot — whichever response landed last won, and when the un-filtered
sibling won, completed topics reappeared in the sidebar despite the
"Include completed" preference being off.

Introduce `useFetchChatTopics` as the single call site for chat-topic
fetching. It reads `topicIncludeCompleted` from preferences and pins
`excludeTriggers` to the always-excluded cron/eval set, so every
sibling mounts with identical args, collapses onto one SWR key, and
SWR dedupes them to a single request. Group sidebars now also exclude
cron/eval triggers for parity with the agent sidebar (groups don't
produce either trigger today, so this is a no-op in practice but
prevents divergence if the rules change).

Popup and mobile-modal call sites keep using the raw `useFetchTopics`
because they deliberately need the unfiltered set — the popup has to
resolve a specific (possibly completed) topic's title from the map.

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

*  feat(chat-input): heterogeneous-agent placeholder for Claude Code sessions

When the active agent is backed by a heterogeneous provider (currently
only `claude-code`), swap the generic "Ask, create, or start a task"
placeholder for a task-specific variant that names the provider
(e.g. "Ask Claude Code to do a task"). @-mention assignment hint is
suppressed in that mode since heterogeneous agents don't yet route to
sibling agents.

* 🌐 chore(i18n): translate sendPlaceholderHeterogeneous (en-US, zh-CN)

Local preview translations for the new heterogeneous-agent chat input
placeholder; en-US mirrors the default, zh-CN carries the Chinese
copy. CI regenerates locale JSON on release so this commit only seeds
dev preview.

* ♻️ refactor(workflow-summary): unify suffix to show total tool kinds and calls

Both branches of getWorkflowSummaryText now share the same suffix structure:
list · 共 N 种工具 · 共 X 次调用 · N 次失败. summaryMoreTools changes from
remaining count ("+N more" / "等 N 种工具") to total count, and the inline
(failed) per-tool marker is dropped in favor of the global error suffix.

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

* ♻️ refactor(workflow-summary): hide redundant kinds/calls suffixes

Show "N tool kinds" only when the displayed list is truncated, and "X calls
total" only when at least one tool was called more than once. Otherwise the
aggregates duplicate information already visible in the per-tool list.

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

* 🎨 style(chat-input): drop hotkey suffix from heterogeneous placeholder

Heterogeneous-agent placeholder (e.g. "让 Claude Code 帮你完成任务…") no
longer trails the "press ⌘↵ to insert a line break" hotkey hint, which read
awkwardly attached to a short single-clause prompt.

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

* 🎨 style(claude-code): align ScheduleWakeup/Task* inspectors with ToolSearch

Drop leading lucide icons, add `:` suffix so the label row reads like
ToolSearch, and promote ScheduleWakeup's `reason` into the chip with
`delaySeconds` trailing as secondary context.

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

* 🐛 fix(heterogeneous-agents): retain subagent tool-call lookup across turn boundaries

`findRunByInnerToolCallId` consulted `run.state.persistedIds`, but that
set is wiped every time `ensureSubagentRun` advances `subagentMessageId`.
A `tool_result` delayed past the owning turn therefore failed the lookup
and skipped the thread-bucket `run.stream.update`, leaving the in-thread
tool bubble stuck on its loading spinner until the user re-opened the
Thread (main-topic `fetchAndReplaceMessages` doesn't rehydrate thread
buckets). Add a run-lifetime `lifetimeToolCallIds` set that only grows
and route the lookup through it; leave `state.persistedIds` as-is so
`persistToolBatch`'s turn-scoped dedupe is untouched.

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-22 13:23:37 +08:00
Arvin Xu 3b306a8aed 🐛 fix(agent-runtime): preserve Gemini 3 thoughtSignature in call_tools_batch normalization (#14032)
The `ToolsCalling` -> `ChatToolPayload` mapping in `runtime.ts` explicitly
enumerated 5 fields and dropped `thoughtSignature`, while the type itself
never declared the field. As a result, any Gemini 3.x tool call beyond
the first one in a conversation would 400 with a misleading
"function call turn must come after user/function response turn" error —
Google's validator maps a missing signature to that generic ordering message.

Fix LOBE-7759.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:14:28 +08:00
Arvin Xu 4af6fddd7a 🐛 fix(context-engine): downgrade image_url parts when target model lacks vision (#14029)
* 🐛 fix(context-engine): downgrade image_url parts when target model lacks vision

Historical messages persisted as multimodal parts (content is an array
with `image_url` entries, or assistant messages with `metadata.isMultimodal`)
bypassed the legacy `imageList` vision check and got forwarded verbatim to
the provider. DeepSeek rejects the `image_url` variant outright, so any
topic containing an image broke the moment the user switched to a
non-vision model.

Replace image parts with a textual placeholder so the conversation still
carries the signal that an image was sent, without including content
non-vision providers reject. Applies uniformly across user array content,
assistant multimodal content, and legacy `imageList` paths.

Fixes LOBE-7214.

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

*  test: update vision-disabled expectations after downgrade placeholder

Two tests in the app suite asserted the silent-drop behavior the
MessageContentProcessor used to exhibit for `imageList` + vision-off:

- src/services/chat/chat.test.ts
- src/services/chat/mecha/contextEngineering.test.ts

After this PR the processor appends the downgrade placeholder instead of
silently dropping the image, so the expected content grows by one line.

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

* 💄 style(context-engine): place vision downgrade placeholder before SYSTEM CONTEXT

The placeholder stands in for an image the user actually sent, so it
should sit adjacent to the user text rather than trailing after the
SYSTEM CONTEXT metadata block. Reorder so the payload reads:

  <user text>

  [image omitted: not supported by this model]

  <!-- SYSTEM CONTEXT ... -->

Keeps the conversational flow intact and matches the semantic position
the image occupied in the original message.

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-22 13:07:42 +08:00
YuTengjing e9600407ff 🐛 fix: reduce subagent task status error noise (#14026) 2026-04-22 12:58:30 +08:00
Arvin Xu f3fca500e4 🐛 fix(heterogeneous-agents): stream subagent Thread + fix parallel-tool orphan (#14024)
*  feat(heterogeneous-agents): stream subagent Thread + fix parallel-tool orphan

When a main-agent step emits a parallel tool_use (e.g. `[Grep, Agent]`),
the gateway handler's stream_chunk branch was forwarding the subagent's
inner `tools_calling` chunks onto `currentAssistantMessageId` (main),
overwriting main.tools[] with subagent tools — main's own Task/Agent
tool_use then had no matching entry and every tool message under it
rendered with the "orphan tool call" banner.

Two coordinated changes:

1. Main-bucket isolation: the executor now drops subagent-tagged
   `stream_chunk` events before forwarding to the gateway handler. DB
   persistence continues via `persistSubagent*Chunk` so the subagent
   content is never lost; only the main-handler in-memory dispatch is
   suppressed for subagent chunks.

2. Thread-bucket streaming: `internal_dispatchMessage` now accepts a
   `threadId` override that snaps scope to `thread`, routing
   create/update payloads to the thread's `messagesMap` bucket. Each
   `SubagentRunState` carries a thread-scoped dispatcher; ensureSubagentRun
   seeds user + assistant on lazy Thread creation and at turn boundaries,
   persistToolBatch gets an `onToolCreated` hook that the subagent path
   uses to seed role:'tool' rows, persistSubagent*Chunk dispatches
   tools[] / content / reasoning updates on every chunk, and the
   tool_result branch mirrors subagent tool_result content (+ pluginState)
   into the thread bucket. Thread view now streams token-by-token with
   the same cadence as the main bubble.

Tests:
- `does NOT forward subagent-tagged stream_chunks to the gateway handler`
  — asserts main bucket isolation under parallel main+subagent tool use.
- `streams subagent create/update dispatches into the thread messagesMap
  bucket` — asserts user/assistant/tool createMessage dispatches land in
  the thread scope, plus streaming updateMessage for tools[], content,
  and tool_result, with no bleed into the main bucket.

Local repro verified end-to-end: main assistant.tools=[Grep, Agent]
stays intact across two parallel runs, thread bucket populates 14 rows
(user + 2 subagent assistants with Bash/Glob then Read×8 + 10 tool
results) during the run, `mainOrphans`/`threadOrphans`/
`threadIntoMainBleed` all empty, orphan warning DOM count = 0.

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

* ♻️ refactor(heterogeneous-agents): route subagent stream through a per-spawn sub-operation

Replace the threadId-override on `internal_dispatchMessage` with a
proper per-spawn child operation, eliminating the second context
expression at the dispatch boundary.

The previous design accepted `{ operationId, threadId? }` and snapped
scope to `'thread'` when the override was present. That was a leaky
parallel path to the operation registry — the same "which messagesMap
bucket should this dispatch hit?" question got answered two different
ways. `startOperation` already supports `parentOperationId` + context
inheritance + recursive cancel cascade, so the right move is to model
the subagent run as a first-class child op and let
`internal_getConversationContext` do its normal job.

Changes:
- Add `'subagentThread'` to `OperationType` (NOT in
  `AI_RUNTIME_OPERATION_TYPES` — it's a context container, not an
  independent loading state, so it shouldn't double-count for spinners).
- `executeHeterogeneousAgent` opens the sub-op in `beginSubagentRun`
  via `startOperation({ type: 'subagentThread', parentOperationId,
  context: { ...context, threadId, scope: 'thread' } })` and binds a
  thread-scoped dispatcher to that sub-op's id.
- `SubagentRunState.subOperationId` carries the id so `finalizeSubagentRun`
  can mark it completed when the spawn's tool_result arrives (or on the
  `onComplete` fallback for crash/abort paths). Cancel cascade + cleanup
  flow through the existing parent/child op linkage.
- Revert the `threadId` override in `internal_dispatchMessage` — the
  store boundary is back to a single context expression
  (`{ operationId? }`).

Test:
- Add `startOperation` mock to `createMockStore` (returns monotonic
  `sub-op-N` ids).
- Update the streaming regression to identify the sub-op via the
  `startOperation` call with `type: 'subagentThread'`, assert the
  sub-op's parent + context shape, filter Thread bucket dispatches by
  `ctx.operationId === subOperationId`, and verify
  `completeOperation(subOperationId)` fires when the run finalizes.

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

* 🐛 fix(heterogeneous-agents): drain subagent buffers only after DB flush confirms

`finalizeSubagentRun`'s buffer reset used to run unconditionally after
the flush try/catch, so a transient `messageService.updateMessage`
failure silently wiped the accumulated streamed text/reasoning — the
later `onComplete` fallback then had nothing left to retry, leaving the
subagent's streamed content absent from persisted thread history.

Move the clear into the success branch. A second concern surfaces once
the clear moves: after the flush block, the `resultContent` branch
advances `currentAssistantMsgId` to the newly created terminal
assistant, so a naive retry that reads `currentAssistantMsgId` would
overwrite the authoritative terminal content with the leftover streamed
buffer — corrupting the subagent summary with stale partial text.

Pin the flush target via a new `SubagentRunState.pendingFlushTarget`:
captured before the DB attempt, carried on the run when the flush
fails, cleared alongside the buffers on success. The retry uses the
pinned target instead of the live `currentAssistantMsgId`, so leftover
streamed buffers always land on the streaming turn's assistant — never
on the terminal row.

Test: `retains subagent buffers + pinned target when the finalize flush
fails` stubs `updateMessage` to throw once for the subagent streaming
write, runs streamed text → spawn `tool_result` → `onComplete`, and
asserts (1) the leftover content eventually reaches DB across ≥2
write attempts and (2) every attempt targets the streaming turn's
assistant — not the terminal row created by `resultContent`.

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 22:18:30 +08:00
AmAzing- 6ddef95249 chore: fix follow-up chat input state during message queueing (#14020)
* 💄 style(chat-input): improve agent assignment placeholder

*  improve follow-up queue input ux

* 💄 sync runtime placeholder locale keys

* Update SKILL.md

* 💄 style(chat-input): hide send menu while generating

Co-Authored-By: Oz <oz-agent@warp.dev>

* fix: ensure sendMenu is correctly cleared in store when prop becomes undefined and add test coverage

---------

Co-authored-by: Oz <oz-agent@warp.dev>
2026-04-21 18:56:52 +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
Arvin Xu f6081c9914 🔨 chore: add headless approval and apiKey WS auth to lh agent run (#13819)
 feat: add headless approval and apiKey ws auth to `lh agent run`

Two fixes so `lh agent run` works end-to-end against the WebSocket agent
gateway when the user is authenticated via LOBEHUB_CLI_API_KEY.

- Default to `userInterventionConfig: { approvalMode: 'headless' }` when
  running the agent from the CLI. Without this flag the runtime waits
  for human tool-call approval and local-device commands hang forever.
  Users who want interactive approval can pass `--no-headless`.
- Pass `tokenType` (`jwt` | `apiKey`) in the WebSocket auth handshake so
  the gateway knows how to verify the token. Previously the CLI sent
  only the raw token value and the gateway assumed JWT, rejecting valid
  API keys.

Fixes LOBE-6939

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:28:01 +08:00
Arvin Xu d6f11f80b6 🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error (#13774)
* 🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error

Production traces across multiple providers (openrouter, openai, google)
surface a single opaque error — `e.trim is not a function` with
`errorType: 'unknown'` — hiding whatever the upstream actually returned.

Root cause: `normalizeCode` / `normalizeErrorType` assumed their input is
always `string | undefined` (matching the TypeScript signature), but real
provider error objects frequently carry a numeric `code` (HTTP status) or
a structured object in `errorType`. `value?.trim()` short-circuits only
on null/undefined, so a truthy non-string turns into a TypeError that
the outer catch records as the "final" error, erasing the upstream one.

Fixes:
- Guard `normalizeCode` / `normalizeErrorType` on `typeof value ===
  'string'`, widen parameter type to `unknown`.
- Wrap the whole `classifyLLMError` in a try/catch that falls back to a
  conservative `stop` decision and preserves the best-effort message of
  the ORIGINAL error. A classifier that throws is worse than a
  classifier that's wrong — it must never shadow the real failure.
- `bestEffortMessage` swallows property-access errors (hostile Proxy
  etc.) to guarantee the fallback itself can't throw.

Regression tests cover: numeric `code`, structured `errorType`, nested
OpenAI-SDK-shaped `error.error.code`, and a hostile Proxy that throws on
every property access.

This is a forcing function for root-cause diagnosis: after this lands,
the real upstream errors behind the 'e.trim' mask will finally surface.

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

* Remove fallback warning in classifyLLMError

Removed console warning for classification failure.

* 🐛 fix(agent-runtime): treat numeric provider code as status fallback

Bare HTTP proxies sometimes surface the HTTP status ONLY as a numeric `code`
on the error object (no `status`/`statusCode`, no digits in the message).
After widening `normalizeCode` to require `typeof === 'string'`, those numeric
codes were dropped entirely and auth/permission failures fell through to
retry — wasting the full retry budget on permanent errors.

Forward numeric `raw.code` / `nested?.code` / `nestedError?.code` into the
status chain (after the real status/statusCode lookups, before the
message-digit extractor) so classifyKind still maps 401/403 → stop and
429/5xx → retry.

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-14 23:23:21 +08:00
Rdmclin2 1c75686b70 🐛 fix: gateway typing error (#13820)
fix: gateway typing error
2026-04-14 23:15:41 +08:00
Arvin Xu 7e89fa782d 🐛 fix: detect truncated tool_calls arguments in builtin tools (#13817)
* 🐛 fix: detect truncated tool_call arguments in builtin tools

When an LLM hits max_tokens mid tool_call, the arguments JSON is
truncated. The previous flow passed `{}` to the tool, which returned a
generic "required field missing" error; the model re-tried with the same
payload and the truncation repeated — one observed trace burned 17 min
and $2.46 on 5 blind retries.

Detect structural truncation (unclosed braces/brackets/strings) in
BuiltinToolsExecutor before schema validation, and return a dedicated
TRUNCATED_ARGUMENTS error telling the model to reduce payload size or
raise max_tokens instead of retrying.

Fixes LOBE-7148

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

* 💄 chore: echo raw arguments string and reject all unparseable JSON

Two improvements based on review:

- Append the received arguments string to the error content so the model
  can verify the payload is exactly what it produced (stops it from
  blaming upstream or guessing what went wrong).
- Treat ANY unparseable non-empty argsStr as an error (new code
  INVALID_JSON_ARGUMENTS), not just truncation. The previous fallback
  of passing `{}` to the tool produced generic "missing field" errors
  that hid the real cause. Empty argsStr still falls through to `{}`
  for tools that take no parameters.

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-14 22:50:49 +08:00
Arvin Xu 18bc2716b2 🔨 fix: wire Gateway-mode stop via direct tRPC interrupt (#13815)
*  feat: wire Gateway-mode stop button to WS interrupt

Frontend half of [LOBE-7142](https://linear.app/lobehub/issue/LOBE-7142)
— the stop button previously silently failed in Gateway mode because:

1. `stopGenerateMessage` only filtered `execAgentRuntime`, so
   `execServerAgentRuntime` ops (Gateway) were skipped.
2. Even if the local op got cancelled, nothing bridged the cancel to
   the server-side agent loop running behind the Agent Gateway WS.

## Changes

**`conversationControl.ts::stopGenerateMessage`** — extend the type
filter to include both op types so both client-side and Gateway-mode
runs are cancelled from the same entry point.

**`gateway.ts::executeGatewayAgent` + `reconnectToGatewayOperation`** —
register an `onOperationCancel` handler on the local `gatewayOpId` that
forwards the server-side operation id to `interruptGatewayAgent(...)`,
which sends `{ type: 'interrupt' }` over the Agent Gateway WS. The
closure cleanly resolves the "local op id vs server op id" mapping —
no metadata lookup needed.

**`operation/actions.ts::cancelOperation`** — `isAborting` flag was
gated on `execAgentRuntime`. Extend to `execServerAgentRuntime` too so
the UI loading state transitions out immediately on Gateway-mode stop,
without waiting for the round-trip `session_complete` from the server.

## What this doesn't do (follow-ups)

- **Backend**: new `POST /api/agent/interrupt` route + Redis LPUSH
  (LOBE-7145). Without it, the WS interrupt reaches Agent Gateway but
  never gets forwarded to cloud.
- **Agent loop**: `AgentRuntimeService.executeStep` LPOP polling of the
  interrupt key (LOBE-7146). Without it, the state never flips to
  `interrupted` server-side.
- **Agent Gateway DO** (external repo): `_forwardInterrupt` HTTP POST
  from the WS interrupt handler (LOBE-7147).

With only this PR merged, clicking stop will clear the local UI state
and send the WS frame correctly — the server-side loop keeps running
until those three are merged too.

## Tests

- `conversationControl.test.ts`: +1 — stopGenerateMessage cancels
  `execServerAgentRuntime`, invokes the onCancel handler, sets
  `isAborting: true`.
- `gateway.test.ts`: +1 — `executeGatewayAgent` registers a handler
  against the local opId, handler invokes `interruptGatewayAgent`
  with the server opId.

All 123 touched-slice tests pass; type-check clean.

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

* 🔨 chore: switch Gateway stop to direct tRPC instead of WS roundtrip

Rewiring only — no new behaviour on top of the previous commit. See
the discussion in PR #13815 for the full reasoning.

TL;DR the WS-based path (client → Agent Gateway WS → DO forwards
HTTP → cloud route → Redis LPUSH → loop LPOP) has the same end-effect
as the tRPC-direct path (client → tRPC → AgentRuntimeService
.interruptOperation → DB state flip), except:

- the tRPC path is one hop instead of three
- the tRPC path reuses infrastructure that's *already on canary* —
  `aiAgentService.interruptTask` → `AiAgentService.interruptTask` →
  `AgentRuntimeService.interruptOperation` → `coordinator.saveAgentState`
  with status='interrupted' — and the existing step-boundary polling
  in `executeStep` (AgentRuntimeService.ts:474, 565) already picks it up
- zero new server code required; zero Agent Gateway (external repo)
  coordination required

The only reason the WS path was in the original spec (LOBE-7142) was
symmetry with the Phase 6.4 tool_execute/tool_result path, but
`interrupt` is a one-shot control signal, not stream data — there's
no actual benefit to routing it through the same channel. Mid-step
abort would require threading an AbortSignal into `runtime.step(...)`,
which WS doesn't help with either.

Closes out the need for LOBE-7145 / LOBE-7146 / LOBE-7147.

Changes:
- `gateway.ts`: both `executeGatewayAgent` and
  `reconnectToGatewayOperation` register the cancel handler against
  the local op id, but the handler body now calls
  `aiAgentService.interruptTask({ operationId: serverOpId })` via
  tRPC instead of `this.interruptGatewayAgent(serverOpId)` (which sent
  the WS interrupt frame).
- `gateway.test.ts`: adjust the one new test case to verify the
  tRPC call rather than the WS-path spy; add `interruptTask` to the
  `aiAgentService` mock.

`AgentStreamClient.sendInterrupt()` and `interruptGatewayAgent()` are
kept as-is — public API, might be useful elsewhere. Just not called
from the cancel handler anymore.

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-14 22:41:45 +08:00
Rdmclin2 636a3b77c3 🐛 fix: message gateway queue error (#13816)
* fix: gateway sync

* fix: skip  error connection

* feat: add disconnect all &  MESSAGE_GATEWAY_ENABLED env vairable

* chore: add gateway test case

* chore: clean lobehub connnections when switch to message gateway

* chore: optimize disconnect all

* chore: disconnect gateway connnections when using lobehub gateway

* chore: clean up exsiting gateway connections after reconnect and avoid gateway callback when not enabled
2026-04-14 22:10:17 +08:00
Arvin Xu c70ac84da7 feat: support run client tools in agent gateway mode (#13792)
*  feat: receive and execute executor=client tools on desktop Electron

Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790,
which adds the `clientRuntime` signal + `hasClientExecutor` gate so
`local-system` and stdio MCP can enter the manifest for desktop callers.

Data flow, client side:

  Agent Gateway WS
     └─ tool_execute event ──► AgentStreamClient
            └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute')
                    └─ internal_executeClientTool (fire-and-forget)
                          ├─ parse args → params
                          ├─ mark pendingClientToolExecutions[toolCallId]
                          ├─ dispatch: builtin → invokeExecutor,
                          │            else   → mcpService.invokeMcpToolCall
                          ├─ clear pending
                          └─ AgentStreamClient.sendToolResult(...)
                                └─ WS → /api/agent/tool-result → LPUSH
                                       → server BLPOP unblocks → loop continues

Key guarantees:

- `internal_executeClientTool` never throws; ALL error paths (parse
  failure, no executor match, thrown executor, missing connection, MCP
  error) still call `sendToolResult({ success: false, error })`. The
  server's BLPOP must never hang on a silent client.
- `case 'tool_execute'` uses `void`, not `await`. A long-running tool
  must not block subsequent `stream_chunk` / `tool_end` events on the
  same WebSocket.
- UI loading state is kept separate from `toolCallingStreamIds` (the
  LLM-streaming animation) via a dedicated
  `pendingClientToolExecutions: Record<toolCallId, true>` map, so a
  renderer can show a distinct "running on device" indicator without
  entangling existing selectors.

Client → server signal:

`executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'`
so the server knows this Electron caller can receive `tool_execute`.

Tests: 39 new cases across AgentStreamClient / internal_executeClientTool
/ gatewayEventHandler covering success, error, MCP fallback, pending
state lifecycle, and fire-and-forget semantics. 148 total in affected
suites.

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

* 🐛 fix: pass server operationId to tool_result dispatch (operationId mismatch)

The gateway event handler received `tool_execute` events but the resulting
`internal_executeClientTool` call looked up `gatewayConnections` by the
*local* operation id (e.g. `op_8chrnd`) instead of the *server-side*
operation id (e.g. `op_1776171452938_...`) the WS connection is actually
keyed on. `conn` was therefore always `undefined`, the early-return in
`send(...)` swallowed the response, and the server's BLPOP waiter timed
out after 60 s.

This was reproducible on canary E2E: server logs showed
`dispatching client tool lobe-local-system/readLocalFile` followed by
`client tool ... timed out after 60027ms`, with no outbound `tool_result`
frame ever reaching the Agent Gateway.

Fix: thread a distinct `gatewayOperationId` through
`createGatewayEventHandler` and use it for the `case 'tool_execute'`
dispatch. The existing `operationId` (used for `dispatchContext` →
`internal_dispatchMessage` keying) is untouched. Both `executeGatewayAgent`
and `reconnectToGatewayOperation` now pass the server id explicitly; when
a caller omits it, it falls back to the local `operationId` for backwards
compatibility.

Verified live on canary: WS now shows
`[in] tool_execute` → `[out] tool_result success=true content=...` and
the agent returns the real local-file contents.

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-14 21:30:13 +08:00
LiJian 116495bd1e 🐛 fix: slove the execAgents tools exec types not correct (#13807)
* fix: slove the execAgents tools exec types not correct

* fix: should inject source:discovery when tools type is lost

* fix: delete the source inject test
2026-04-14 17:51:08 +08:00
LiJian 922f7ace41 🐛 fix: fixed the when call saveCreds the bad request problem (#13809)
* fix: fixed the when call saveCreds the bad request problem

* fix: add the empty kv checked
2026-04-14 17:51:00 +08:00
YuTengjing b369c53bda 🐛 fix(model-bank): disable GLM-5.1 built-in search in LobeHub (#13806) 2026-04-14 17:05:42 +08:00
René Wang 5ecccf4b9e 📝 docs: add April 13 weekly changelog (#13808) 2026-04-14 17:02:10 +08:00
Rdmclin2 f9fbd45fee feat: discord support slash commands and DM (#13805)
* fix: slack not respond to text commands

* feat: add slack slash commands instructions

* chore: add slack validate in test connections

* chore: update slack docs

* chore: remove text commands for slack
2026-04-14 16:48:16 +08:00
LiJian 0b490a7268 🐛 fix: execAgent should get builtin discoverable tools into manifests (#13804)
* fix: execAgent should get all tools manifests

* fix: should add the tools source into payload source

* fix: add the discoverable tools into tools enginer

* fix: update the test, should include the discoverable tools
2026-04-14 16:07:49 +08:00
Innei a9c5badb80 ♻️ refactor(navigation): stable navigate hook and imperative routing (#13795)
*  fix: implement stable navigation hook and refactor navigation handling

- Introduced `useStableNavigate` hook to provide a stable `navigate` function that can be used across the application.
- Refactored components to utilize the new stable navigation approach, replacing direct access to the navigation function from the global store.
- Updated `NavigatorRegistrar` to sync the `navigate` function into a ref for consistent access.
- Removed deprecated navigation handling from various components and actions, ensuring a cleaner and more maintainable codebase.

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

* 🐛 fix: refactor navigation handling to prevent state mutation

- Updated navigation reference handling in the global store to use a dedicated function for creating navigation refs, ensuring that the initial state is not mutated by nested writes.
- Adjusted tests and components to utilize the new navigation ref creation method, enhancing stability and maintainability of navigation logic.

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

*  test: mock Electron's net.fetch in unit tests

- Added a mock for Electron's net.fetch in the AuthCtr and BackendProxyProtocolManager tests to ensure proper handling of remote server requests.
- This change allows tests to simulate network interactions without relying on the actual fetch implementation, improving test reliability.

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

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-14 13:28:12 +08:00
LiJian cd0f65210c ♻️ refactor: update the codesandbox systemRole(preinstalled_software) (#13799)
refactor: update the codesandbox systemRole(preinstalled_software)
2026-04-14 12:11:44 +08:00
Arvin Xu 24be35fd84 🐛 fix(agent-runtime): resolve S3 image keys when refreshing messages (#13794)
messageModel.query() calls inside RuntimeExecutors were missing a
postProcessUrl callback, so imageList/videoList/fileList entries retained
raw S3 keys (e.g. `files/user_xxx/icon.png`). After the first tool batch,
the refreshed state fed those raw keys straight into the next LLM call,
and providers like Anthropic reject anything that isn't an absolute URL or
data URI ("Invalid image URL"). Wire a lazy FileService-backed
postProcessUrl into all three query sites (topic reference resolution,
compression, and post-batch refresh) so imageLists stay resolved across
multi-step operations.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:29:49 +08:00
Arvin Xu 46adf43453 🐛 fix: dispatch executor=client tools to desktop callers when DEVICE_GATEWAY is configured (#13793)
🐛 fix: dispatch executor=client tools to desktop caller even with DEVICE_GATEWAY configured

Two fixes to make Phase 6.4 (LOBE-7076) actually reach a desktop caller on
canary, where DEVICE_GATEWAY is configured and a separate remote device
may be registered.

### 1. AgentToolsEngine: suppress RemoteDevice for desktop callers

The `lobe-remote-device` tool is meant for the legacy "tunnel commands to
a separately registered desktop" flow. When the caller itself is a
desktop Electron client, that's redundant — and worse, the LLM was
picking `listOnlineDevices` + `activateDevice` *first*, then routing the
subsequent `readLocalFile` to a different registered host (a remote
Linux VM in our E2E trace, returning ENOENT for a path that only exists
on the caller).

Adds `&& !hasClientExecutor` to the RemoteDevice enable rule. Desktop
callers now see only `local-system` in their manifest.

### 2. aiAgent.execAgent: mark executor='client' for desktop callers

The existing gate was `if (!gatewayConfigured) { executorMap[...] = 'client' }`.
On canary, `gatewayConfigured === true` (DEVICE_GATEWAY set), so
`local-system` / stdio MCP stayed server-executed and were dispatched to
the Remote Device proxy instead of back to the caller's Agent Gateway WS.

Extends the gate to:
  `if (clientRuntime === 'desktop' || !gatewayConfigured)`

So a caller that explicitly signals it can receive `tool_execute` bypasses
the DEVICE_GATEWAY heuristic. Legacy behaviour unchanged for web callers
and for callers that don't send `clientRuntime`.

### Tests

- AgentToolsEngine: +1 case verifying RemoteDevice is suppressed when
  `clientRuntime === 'desktop'` even with `gatewayConfigured: true`
- execAgent.deviceToolPipeline: +3 cases
  - local-system gets executor='client' for desktop + DEVICE_GATEWAY
  - stdio MCP gets executor='client' for desktop + DEVICE_GATEWAY
  - web caller preserves legacy routing (executor unset)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:22:18 +08:00
Arvin Xu f0a811ef83 🐛 fix: enable executor=client tools for desktop Electron callers (#13790)
*  feat: enable executor=client tools for desktop Electron callers

Adds a `clientRuntime` signal to execAgent so the server knows the caller
itself can execute `executor: 'client'` tools (local-system, stdio MCP) over
its Agent Gateway WebSocket. This is the missing server piece for Phase 6.4
(LOBE-7076): previously `local-system` only entered the manifest when a
*separately registered* remote device was online & auto-activated, so a
desktop Electron caller sitting on the other end of the Gateway WS could
never actually be dispatched to via `tool_execute`.

The new signal is orthogonal to the legacy device-proxy `deviceContext` —
it describes the caller itself, not a third-party device. The enable rule
for LocalSystemManifest simply gets one extra OR branch:

  local && gatewayConfigured && (hasClientExecutor || legacy-device-online-activated)

`toolExecutorMap[LocalSystemManifest.identifier] = 'client'` (LOBE-7067)
then kicks in as soon as the manifest entry is present, so
`RuntimeExecutors.call_tool` (LOBE-7068) will push `tool_execute` over the
Agent Gateway WS to this caller.

Plumbing:
- packages/types: `ExecAgentParams.clientRuntime?: 'desktop' | 'web'`
- lambda router: accepts + forwards `clientRuntime`
- aiAgent service: forwards to `createServerAgentToolsEngine`
- AgentToolsEngine: +1 field, +1 OR branch in LocalSystem enable rule.
  Zero changes to `runtimeMode` / `platform` / `RemoteDeviceManifest` /
  `deviceContext` semantics.

Tests: 3 new cases in AgentToolsEngine covering desktop / web / gateway-off
branches; 3 new cases in execAgent.deviceToolPipeline verifying the
`clientRuntime` param is forwarded verbatim.

Follow-up (separate PR): frontend receives `tool_execute`, runs the tool
via Electron IPC, and sends `tool_result` back over the same WS.

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

* ♻️ refactor: untangle runtime / platform / device-proxy flags in AgentToolsEngine

Renames and separates two orthogonal concerns that used to share the
misleading `isDesktopClient` name:

- `hasClientExecutor` — caller itself can receive `tool_execute` over
  the Agent Gateway WS (Phase 6.4). Property of the caller.
- `hasDeviceProxy` — server has a device-proxy configured that tunnels
  to a separately registered device (legacy Remote Device). Property of
  the server.

`platform` is now derived from the caller (`clientRuntime`) first,
falling back to the device-proxy signal for backwards compat — it was
previously derived purely from the server's proxy config, which
conflated "server can reach a desktop" with "caller is a desktop".

LocalSystem enable rule restructured to read in natural order:
  runtimeMode === 'local'         // user opted in
  && hasDeviceProxy               // server has a Gateway path
  && (hasClientExecutor || ...)   // an execution target exists

Behavior is identical to the previous commit; this is a pure rename /
regrouping refactor. 38 existing tests still pass without changes.

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

* 🐛 fix: decouple hasClientExecutor from hasDeviceProxy in local-system gate

The previous rule required `hasDeviceProxy` as a shared prerequisite for
BOTH enable paths, which is wrong: `hasDeviceProxy` reflects the legacy
device-proxy (`deviceProxy.isConfigured`), while Phase 6.4's
`tool_execute` rides the Agent Gateway WebSocket that this request is
already on. The two systems are orthogonal — a desktop caller on the
Gateway WS can receive `tool_execute` without any device-proxy being
configured server-side.

Correct enable rule:

  runtimeMode === 'local'
  && (hasClientExecutor                              // Phase 6.4, self
      || (hasDeviceProxy && deviceOnline && autoActivated))  // legacy

Updated the `still requires gateway to be configured` test, which was
asserting the incorrect coupling, to instead verify that agent-level
`runtimeMode.desktop === 'none'` opt-out is respected for desktop
callers.

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-14 01:12:45 +08:00
Arvin Xu 10914ff015 🐛 fix: add image-to-video options to CLI generate video command (#13788)
*  feat: add image-to-video options to CLI generate video command

Why: CLI only supported text-to-video. Backend already accepts imageUrl/endImageUrl
for image-to-video, but the CLI had no way to pass them.

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

* update cli version

* update cli version

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:12:10 +08:00
Innei b9c4b87a90 🩹 fix(database): enforce document history ownership and pagination 2026-04-14 00:51:13 +08:00
Adam Bellinson b857ae6c57 🐛 fix(desktop): use Electron net.fetch for remote server requests (#13400)
* use Electron's net.fetch() so system trusted certs are honored

* 🐛 fix(tests): mock netFetch in unit tests broken by net.fetch migration

Both LocalFileCtr and RemoteServerConfigCtr tests were patching
global.fetch / stubGlobal, which no longer intercepts calls now that
the controllers route through Electron's net.fetch via @/utils/net-fetch.
Hoist the fetch mock and point vi.mock('@/utils/net-fetch') at it directly.
2026-04-14 00:45:54 +08:00
Arvin Xu e11c89fc48 🐛 fix(agent-runtime): skip client-executor marking when gateway is configured (#13787)
Tools flagged as `executor: 'client'` are dispatched via `dispatchClientTool`
through the Agent Gateway WS path. In cloud deployments where the gateway is
configured but no desktop device is connected, this path 404s on
`/api/operations/tool-execute` and the tool fails with `dispatch_failed`.

Only mark local-system and stdio MCP plugins as `'client'` when the gateway
is NOT configured (standalone Electron). When deviceContext is available,
tool routing goes through the RemoteDevice proxy instead.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:25:19 +08:00
Innei e3eef044ca 👷 build(database): add document history schema 2026-04-14 00:05:06 +08:00
LiJian b9a136f9f1 🐛 fix: slove the execAgent not have lobehub skills & builtin tools (#13781)
fix: slove the execAgent not have lobehub skills & builtin tools
2026-04-13 18:15:54 +08:00
Rdmclin2 809e1e0716 🐛 fix: message gateway ensure running (#13780)
fix: message gateway ensure running
2026-04-13 17:43:18 +08:00
Octopus 7953cf5b5a fix(desktop): use low urgency for Linux notifications to prevent GNOME Shell freeze (#13767)
🐛 fix(desktop): use low urgency for Linux notifications to prevent GNOME Shell freeze

On Linux/GNOME Shell, desktop notifications with urgency 'normal' appear
as banner pop-ups. Clicking the dismiss (X) button on these banners can
cause the system to freeze for 30-45 seconds due to heavy gnome-shell
CPU and memory usage.

Setting urgency to 'low' on Linux routes notifications to the message
tray instead of displaying them as banners, which avoids the problematic
X button interaction. The urgency option is ignored on macOS and Windows.

Fixes #13538

Co-authored-by: octo-patch <octo-patch@github.com>
2026-04-13 16:19:44 +08:00
LobeHub Bot 012214205e 🌐 chore: translate non-English comments to English in database-tests (#13771)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:14:09 +08:00
Arvin Xu f0f2feb015 🔨 chore(task): add participants to task.list response (#13778)
*  feat(task): add participants array to task.list response

Return a participants array per task (id / type / avatar / name) so
clients can show avatar groups on task cards. For now participants
only contains the assignee agent; future iterations can aggregate
comment authors and topic executors.

Also extract TaskItem into @lobechat/types as an explicit type
definition so it no longer relies on drizzle schema inference.

* ♻️ refactor(task): extract NewTask to @lobechat/types

Remove the drizzle $inferInsert NewTask from schemas and define it
explicitly in @lobechat/types alongside TaskItem.

*  test(task): cover participants in task.list response
2026-04-13 16:09:53 +08:00
Innei f439fb913a 🐛 fix(editor): bump @lobehub/editor to 4.8.1 (#13756)
🐛 fix: bump @lobehub/editor to 4.8.1
2026-04-13 14:17:39 +08:00
Neko 6966d366d1 🐛 fix(userMemories): should trim way too long bm25 (#13744) 2026-04-13 13:45:37 +08:00
LiJian f89adb36b3 🐛 fix: slove the agent details pages not get the agent config always lo… (#13772)
fix: slove the agent details pages not get the agent config always loading problem
2026-04-13 12:46:10 +08:00
Arvin Xu 3c2fc7f368 🔨 chore(agent-runtime): dispatch client-executor tools via Agent Gateway WS (#13769)
 feat(agent-runtime): dispatch client-executor tools via Agent Gateway WS

Wire the block-await dispatch path for tools marked as `executor: 'client'`:

- `aiAgent/index.ts` (6.3a) — derive `toolExecutorMap` from manifests:
  * `local-system` builtin → `'client'` (requires Electron IPC)
  * MCP plugins with `customParams.mcp.type === 'stdio'` → `'client'`
    (subprocess runs on the user's machine)
  Purely manifest-driven; no new context / capability fields needed.

- `dispatchClientTool` (6.3b) — helper that:
  * Pushes a `tool_execute` event via `streamManager.sendToolExecute`
  * Block-awaits on Redis BLPOP via `ToolResultWaiter`
  * Returns a `ToolExecutionResultResponse`-shaped object (drop-in with
    the existing server path)
  * Never throws — timeouts / gateway errors / missing infra all
    produce a failed-but-structured result so the agent loop continues

- `RuntimeExecutors.call_tool` / `call_tools_batch` — route to
  `dispatchClientTool` when `payload.executor === 'client'` AND the
  stream manager exposes `sendToolExecute`. Otherwise fall through to
  the existing server path unchanged. Response API (`source: 'client'`)
  interrupt branch is untouched.

Capped at 270s per tool to match Vercel's streaming function window;
longer tools will be handled by the resumable path in Phase 6.3c.

Covered by:
- 5 unit tests on `dispatchClientTool` (gateway missing, redis missing,
  happy path, timeout, dispatch error)
- 286 existing tests still pass in adjacent suites

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:25:43 +08:00
LobeHub Bot a72ae190a3 🌐 chore: translate non-English comments to English in integration-test-utilities (#13749)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 11:01:34 +08:00
Arvin Xu 4feafb3fcb ♻️ refactor: migrate memory-user-memory workflows to Hono (#13768)
Replace 6 per-path Next.js `route.ts` handlers (using `@upstash/workflow/nextjs` serve) with a single Hono app mounted at `[[...route]]`. Workflow logic moves to `src/server/workflows-hono/memory-user-memory/`; all public URLs remain unchanged so existing `MemoryExtractionWorkflowService.triggerXxx` callers need no update.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:50:10 +08:00
Arvin Xu eff527de65 🔨 chore(agent-runtime): add ToolResultWaiter for BLPOP-based tool result await (#13763)
*  feat(agent-runtime): add ToolResultWaiter for Redis BLPOP-based tool result await

Introduce ToolResultWaiter — a Promise-based wrapper around Redis BLPOP
that server-side agent loops will use to block-await client-side tool
execution results delivered via the callback API (LPUSH on another
connection).

Design highlights:
- Takes two ioredis clients: a dedicated blocking connection for BLPOP
  (must not be shared with business traffic) and a normal producing
  connection for side effects (cancel sentinel).
- `waitForResult(id, timeoutMs)` returns the parsed payload or null on
  timeout / cancel, never throws for timeout (caller decides fallback).
- `waitForResults(ids[], timeoutMs)` fans out via Promise.all, aligning
  results with input order.
- `cancel(id)` LPUSHes a poison-pill sentinel to wake a pending waiter,
  used when the agent loop is terminated mid-tool.

Covered by unit tests (6 cases: push-before / push-after / timeout /
batch / cancel / malformed payload).

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

* 🐛 fix(agent-runtime): use multi-key BLPOP in waitForResults to avoid N×timeout latency

Promise.all-ing waitForResult over a shared blocking Redis connection
actually serializes: BLPOP holds the socket, so calls run back-to-back
rather than concurrently. A batch of N where some results never arrive
would take up to N × timeoutMs to resolve, stalling tool-call loops
and delaying cancellation.

Rewrite waitForResults to use Redis's multi-key BLPOP in a loop with a
shared deadline: each iteration blocks on all remaining keys with the
remaining budget, wakes when any one arrives, drops that key, and
re-enters with the rest. Total latency is bounded by one timeoutMs
regardless of N. Single-key waitForResult now delegates to this path.

Covered by a new regression test asserting that an N=3 batch of
never-arriving keys completes in ~1 timeout window, not N×.

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-13 10:47:49 +08:00
Arvin Xu c60563fffc 🔨 chore(api): add POST /api/agent/tool-result callback endpoint (#13764)
 feat(api): add POST /api/agent/tool-result callback endpoint

Agent Gateway forwards client tool execution results to this endpoint;
the handler LPUSHes into a per-toolCallId Redis list with a 120s TTL so
the server-side agent loop's BLPOP can wake and continue.

- Auth via AGENT_GATEWAY_SERVICE_TOKEN bearer header
- Zod-validated body: { toolCallId, content, success, error? }
- Key: tool_result:{toolCallId}
- Idempotency not required; duplicates sit under TTL until expired

No runtime caller yet — wiring lands with the BLPOP waiter in LOBE-7068.

Covered by unit tests (6 cases: missing/wrong token, missing token env,
invalid body, Redis unavailable, happy path, Redis write error).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:35:03 +08:00
Arvin Xu b36c5a2f1b 🔨 chore: add GatewayStreamNotifier.sendToolExecute (#13765)
 feat(agent-runtime): add GatewayStreamNotifier.sendToolExecute

Expose a request-response-style push for tool_execute on top of the
existing Gateway HTTP pipe. Callers use this to delegate tool execution
to the client; failures surface back to the caller so the agent loop
can decide whether to fall back to the interrupt-resume path.

- `IStreamEventManager.sendToolExecute?` — optional interface method,
  only the Gateway-backed notifier implements it (InMemory/Redis-only
  managers intentionally leave it undefined)
- `GatewayStreamNotifier.sendToolExecute(operationId, ToolExecuteData)`
  POSTs to Gateway `/api/operations/tool-execute`
- New private `httpPostAwait` helper preserves the 5s timeout but,
  unlike the fire-and-forget `httpPost`, rejects on non-ok / network
  failure so callers can react

No runtime caller yet; the dispatch branch lands with LOBE-7068.

Covered by unit tests (3 new cases: happy path payload, non-ok
response, network error).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:29:43 +08:00
Arvin Xu 12bbc56db3 🔨 chore: add tool_execute / tool_result protocol types (#13762)
*  feat(agent-stream): add tool_execute / tool_result protocol types

Introduce the type-level scaffold for the Gateway-mediated client tool
execution flow:

- `tool_execute` server→client event with `ToolExecuteData` payload
  (toolCallId, identifier, apiName, arguments, executionTimeoutMs)
- `tool_result` client→server message with success/error and content,
  added to the `ClientMessage` union

No runtime wiring yet; this PR is pure type scaffolding so subsequent
server (Redis BLPOP waiter, Gateway notifier, RuntimeExecutors branch)
and client (gateway handler) work can land independently.

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

* Update types.ts

* 💄 style(agent-stream): reorder ToolResultMessage fields for perfectionist

Move `error?` before `state?` to satisfy `perfectionist/sort-interfaces`
after the `state?: any` field was added to align with ChatToolResult.

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-13 10:09:53 +08:00
Rdmclin2 73be58ba12 feat: support message gateway (#13761)
* feat: support message gateway

* feat: sync message gateway connections

* chore: add cloudflare http v2

* fix: typing interval

* feat: add connnectionMode to gateway

* chore: add applicationId when connect

* fix: judge typing supoort by  triggerTyping implementation

* fix: skip message gateway & start connnections

* fix: qq platform hint

* chore: skip webhook mode in gateway connection

* fix: test case

* fix:  message gateway check

* chore: add failaure case post

* fix: test case

* feat: add GatewayManager for webhook-mode platforms
2026-04-13 01:27:54 +08:00
Arvin Xu 3ad124ac4f 🔨 chore: support multimodal input for server-side agent execution (#13759)
*  feat(agent): support multimodal input for server-side agent execution

Wires already-uploaded file IDs through the Gateway-mode execAgent path so
SPA-attached images / documents / videos reach the LLM when the agent runs
server-side. Resolves attachments via FileModel.findByIds, classifies by
MIME, parses documents idempotently, and persists the messages_files link
for history replay.

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

* 🐛 fix(agent): dedupe repeated fileIds before writing messages_files

messages_files has a composite PK on (file_id, message_id); a fileIds array
containing the same id twice would fail the insert and abort execAgent. Dedupe
the input while preserving caller-provided order so rendering stays stable.

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-13 00:52:03 +08:00
Arvin Xu e569c8dee0 ♻️ refactor: introduce ToolExecutor field orthogonal to ToolSource (#13760)
Add ToolExecutor ('client' | 'server') as a new orthogonal dimension
alongside ToolSource to describe where a tool invocation is dispatched.
Thread executorMap through OperationToolSet / ResolvedToolSet / AgentState
and attach executor to the ChatToolPayload emitted in onToolsCalling.

Defaults remain empty (all server-side), so behavior is unchanged. This
is pure scaffolding to unblock subsequent work on client-side dispatch.

Also remove the unused 'plugin' value from ToolSource (no downstream
consumers branched on it; installed plugins now labeled 'mcp').

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:28:30 +08:00
YuTengjing 406cb5554b feat: add GLM-5.1 model support for Zhipu provider (#13757) 2026-04-12 22:14:52 +08:00
Arvin Xu 0486be4773 🐛 fix: guard non-string content in context-engine to prevent e.trim errors (#13753)
🐛 fix: guard non-string content in context-engine to prevent `e.trim is not a function`

Two unguarded `.trim()` / string-concatenation paths in the context-engine
could throw or produce garbage text when a message's `content` is not a
plain string (multimodal parts array, null tool turns). Both are reached
in normal chat and trigger `e.trim is not a function` in production.

- `resolveTopicReferences`: filter out non-string content in the fallback
  `lookupMessages` path before calling `.trim()`. Without this guard, the
  outer try/catch swallows the TypeError and drops the whole fallback.
- `MessageContent` processor: normalize `message.content` (string or
  parts array) before concatenating file context, instead of relying on
  implicit `toString()` coercion which emitted `[object Object]` into
  the LLM prompt.

Adds regression tests for both paths.
2026-04-12 19:27:52 +08:00
Innei f2ee67c3c5 🐛 fix(inbox): restore inbox avatar fallback after deletion (#13752) 2026-04-12 17:40:40 +08:00
Rdmclin2 16ed80701c 🐛 fix: revert anthropic base64 image (#13751)
chore: revert anthropic base64 image
2026-04-12 16:06:19 +08:00
Octopus 37bf1bd191 fix(local-system): restore loc param when calling readLocalFile IPC (#13748)
🐛 fix(local-system): restore loc param when calling readLocalFile IPC

The `denormalizeParams` method in `LocalSystemExecutionRuntime` was
missing a case for `readLocalFile`. It fell through to `default`, which
passed `{startLine, endLine, path}` as-is to the IPC layer. However,
the IPC handler (`LocalFileCtr.readFile`) expects `LocalReadFileParams`
with `loc?: [number, number]`, not `startLine`/`endLine`. As a result,
`loc` was always `undefined` on the IPC side, causing `readLocalFile`
to default to `[0, 200]` and always return content from line 0.

Fix: add an explicit `readLocalFile` case that reconstructs the `loc`
tuple from `startLine` and `endLine` before forwarding to the IPC layer.

Fixes #13735

Co-authored-by: octo-patch <octo-patch@github.com>
2026-04-12 14:34:42 +08:00
Neko e0f97c4920 🐛 fix(userMemories): missing cancel webhook api for cascading cancellation (#13742) 2026-04-12 04:35:17 +08:00
Arvin Xu 93698f76f8 🔨 chore: update cli version (#13741)
update cli
2026-04-12 02:20:08 +08:00
Arvin Xu 2c79b5ab78 🐛 fix: refine ProviderBizError classification for insufficient balance and quota limit (#13740)
* 🐛 fix: refine ProviderBizError classification for insufficient balance and quota limit errors

Extract inline "Insufficient Balance" check into a dedicated `isInsufficientQuotaError` utility with case-insensitive matching and broader patterns. Add "too many tokens" pattern to `isQuotaLimitError` for Moonshot rate-limit messages.

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

* update

* 🐛 fix: remove "account has been deactivated" from InsufficientQuota patterns

Account deactivation can be triggered by policy, security, or account review — not just billing. Classifying it as InsufficientQuota misleads users into topping up balance when the fix is usually permission or support escalation.

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

*  feat: add AccountDeactivated error type for deactivated/suspended accounts

Separate account deactivation from InsufficientQuota so users get actionable guidance (contact support) instead of misleading billing advice.

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-12 02:14:30 +08:00
Rylan Cai 5613935b73 🐛 fix: fix cli message/topic list page indexing (#13731)
* 🐛 fix cli message/topic list page indexing

* ♻️ inline page parsing in message command
2026-04-12 00:46:31 +08:00
Arvin Xu fb7f0c3e92 🐛 fix: preserve error message in ChatCompletionErrorPayload (#13736)
* 🐛 fix: preserve error message in ChatCompletionErrorPayload for ProviderBizError

Add `message` field to `ChatCompletionErrorPayload` and extract SDK error messages in `handleOpenAIError` and `handleAnthropicError`, so downstream consumers (agent tracing, error state) receive human-readable error details instead of generic "ProviderBizError".

Closes LOBE-7019

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

* 🐛 fix: guard nullish error in handleAnthropicError

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-11 23:42:03 +08:00
Rdmclin2 08769e5bf1 🐛 fix: layout recent locale and support dismiss banner (#13739)
* fix: CN locale for rencents

* fix: community profile setup modal

* feat: support skill banner dismiss
2026-04-11 23:27:21 +08:00
Arvin Xu 732a3ae54a ♻️ refactor: clean up unused sessionStore selectors and slices (#13738)
Remove dead code from the legacy sessionStore:
- Delete `recent` slice (migrated to homeStore)
- Delete `homeInput` slice (migrated to homeStore)
- Remove unused selectors: currentSessionSafe, hasCustomAgents,
  defaultSessionsCount, defaultSessionsLimited, getSessionMetaById,
  currentGroupMeta, getDescription
- Update store type, initialState, and tests accordingly

Closes LOBE-7018

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:37:22 +08:00
Arvin Xu a8fee05c3e 🔨 chore: resolve author info for task activity list (#13732)
*  feat: resolve author info (avatar + name) for task activity list

Add `author` field to `TaskDetailActivity` with `{id, type, name, avatar}`.
Backend resolves agent/user info via batch queries in `getTaskDetail`:
- Topics: author is the task's assignee agent
- Briefs: author is the brief's agentId
- Comments: author is authorAgentId or authorUserId

Fixes LOBE-7013

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

* ♻️ refactor: move author resolution queries to model layer

Replace direct db.select() calls in TaskService with:
- AgentModel.getAgentAvatarsByIds() for agent info
- UserModel.findByIds() for user info

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-11 20:43:00 +08:00
Arvin Xu c255bfe97d 💄 style: show loading state for assistant message during optimistic update (#13733)
🐛 fix: show loading state for assistant message during sendMessage phase

During optimistic update, the assistant message content is "..." but the
loading indicator was not shown because isGenerating only checks
AI_RUNTIME_OPERATION_TYPES (execAgentRuntime), not sendMessage. Include
isCreating state so the loading dots appear immediately when message is sent.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:37:16 +08:00
Arvin Xu f7f2e063d1 💄 style: add delete action to agent profile dropdown menu (#13734)
*  feat: add delete action to agent profile dropdown menu

Add a "Delete" option to the three-dot menu in Agent Profile header,
with confirmation modal. Uses existing `removeAgent` from homeStore.

Fixes LOBE-6582

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

* 🐛 fix: navigate to home after deleting agent from profile

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-11 20:31:55 +08:00
Sun13138 39bca4bc1a 🐛 fix(gemini): align thinkingLevel config resolution across the stack (#13457)
* 🐛 fix(gemini): omit empty thinkingConfig and align thinkingLevel keys

- Google runtime: omit empty thinkingConfig to avoid sending thinkingConfig: {} upstream.\n- UI: ThinkingLevel2/3/4/5 sliders read/write only their own config key.\n- Resolver: map model extend params thinkingLevel* to matching chatConfig key (no fallback/priority logic).\n- Tests: add regression coverage for empty thinkingConfig omission.

* 🐛 fix(gemini): restore default thinking levels by model param

* 🐛 fix(gemini): prefer configured thinkingLevel params
2026-04-11 19:10:26 +08:00
Arvin Xu 9b765eb360 update og 2026-04-11 18:43:28 +08:00
Arvin Xu f68c45cab6 Merge remote-tracking branch 'origin/canary' into fix/task-topic-status-cascade 2026-04-11 18:42:57 +08:00
LobeHub Bot 44c569c5db 🌐 chore: translate non-English comments to English in chat store (#13728)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 18:37:00 +08:00
Arvin Xu 390d82d730 🐛 fix: complete operation and show error on gateway error event (#13714)
* 🐛 fix: complete operation and show error on gateway error event

- Error event handler writes inline error immediately via
  internal_dispatchMessage, then fetches from DB for richer detail.
  This ensures the UI always shows an error even when the server
  hasn't persisted the error into the message table.
- disconnected listener only fires onSessionComplete after a terminal
  agent event (agent_runtime_end / error), not on auth failures or
  explicit disconnect calls.
- Track terminal events via agent_event listener with dedup guard to
  prevent double-firing onSessionComplete.

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

* 🐛 fix: persist error into assistant message on agent runtime failure

When an agent runtime step fails, the error was written to error_logs
and Redis state but not to the assistant message in the DB. This caused
the frontend to show an empty message after fetchAndReplaceMessages,
since the message had no error field set.

Now dispatchCompletionHooks writes the error to the assistant message
via messageModel.update when reason is 'error', matching the pattern
used by updateAbortedAssistantMessage.

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-11 18:22:18 +08:00
Arvin Xu 2b44cdd298 🐛 fix: add null guard for topicId to fix type error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:05:41 +08:00
Arvin Xu 345f144b1e 🐛 fix: use conditional cancel and fail-fast on interrupt errors
- Add `cancelIfRunning` to TaskTopicModel: atomically cancel only if topic
  is still running, preventing overwrite of concurrent completed/timeout transitions
- Skip topic cancellation when `interruptTask` fails, keeping DB state
  consistent with the still-running remote operation
- Add test for interrupt failure scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:52:29 +08:00
YuTengjing f780f43863 🔨 chore: fix skill frontmatter key to use kebab-case (#13730) 2026-04-11 15:53:02 +08:00
Rdmclin2 ac1abbaf8b 🐛 fix: bot error lobe 6925 (#13724)
* chore: remove unused variables

* fix: add  catch error

* chore: use url for anthropic image

* feat: add bot  process warnings to context

* feat: add thread context

* fix: rename thread name when already has one

* chore: update test cases

* fix: warning sanitize

* fix: threadName safe review
2026-04-11 02:11:33 +08:00
Innei b5f98bd745 feat(chat-input): improve mention menu skill and tool icons (#13722)
*  feat(chat-input): improve mention menu skill and tool icons

- Add MentionItemIcon with SkillAvatar, McpIcon, and Avatar fallbacks
- Strip placeholder avatars ending with _AVATAR
- Tweak mention item icon frame (overflow, border-radius)

Made-with: Cursor

* 💄 fix(chat-input): use theme-aware mention skill fallback
2026-04-11 01:35:02 +08:00
Innei 48d0a759a8 🐛 fix(chat): refine workflow collapse headline (#13717)
* 🐛 fix(chat): refine workflow collapse headline

* 🐛 fix(chat): use state machine for workflow headline

* 🐛 fix(chat): backtrack workflow headline state

* ♻️ refactor(chat): simplify workflow headline selector

* 💄 style(chat): use lucide workflow collapse arrow

* ♻️ refactor(chat): use accordion indicator layout

* Move workflow duration text beside the title

* Localize workflow tool display labels

* Update Page workflow localization labels

* fix: sort imports in toolDisplayNames.test.ts
2026-04-11 00:49:25 +08:00
Rdmclin2 5d135b3ae1 🔨 fix: layout sidebar en More (#13723)
* fix: layout sidebar en More

* chore: update i18n files
2026-04-11 00:46:53 +08:00
Tsuki 17b3acead6 feat(subscription): add cross-platform subscription support for mobile IAP (#13413)
feat(subscription): add cross-platform subscription i18n and mobile subscription router

- Add crossPlatform.title/desc/manageOnMobile translations for 18 languages
- Register mobileSubscriptionRouter in mobile tRPC router
- Add mobileSubscription business router placeholder

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:27:51 +08:00
Rdmclin2 2c397390b4 feat: layout sidebar impl (#13719)
* ♻️ Restructure sidebar layout: extract Lobe AI entry, move New Agent button

- Extract Lobe AI (InboxItem) from agent list to standalone top entry in sidebar body
- Move "New Agent" button from header to below Lobe AI entry
- Add "Create" to bottom menu items alongside Community and Resources
- Filter hidden items in BottomMenu component

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Add unified Recents section to home page

- New TRPC router `recent.getAll` aggregating topics, documents, files, and tasks
- New client service and SWR-based store integration for recents data
- Unified Recents component on home page with type-based icons
- Items sorted by updatedAt, limited to 10, mixed across all types

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Prefetch agent config on hover for faster page loads

- Add usePrefetchAgent hook using SWR mutate to warm cache
- Trigger prefetch on mouseEnter for sidebar agent items
- Reduces or eliminates loading screen when navigating to agent pages

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Redesign agent homepage with info, recent topics, and tasks

- New AgentHome feature replacing the old AgentWelcome component
- Agent info section: avatar, name, description, opening questions
- Recent Topics: horizontal scrollable cards for agent-specific topics
- Tasks section: list with status labels for agent-assigned tasks
- Preserve ToolAuthAlert for tool authorization flows

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

* fix: common misstakes in layout

* chore: add fetch Recents cache

* chore: add back createagents

* chore: add back lobe ai

* feat: add display count

* feat: add create agent button

* feat: add sidebar section order

* chore: move divider

*  feat: show current page size in display items submenu

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

*  feat: add sidebar display management with customize sidebar modal

- Add "Hide section" and "Customize sidebar" to Recents/Agents dropdown menus
- Create CustomizeSidebarModal with eye toggle for section visibility
- BottomMenu (Community/Resources) also manageable via modal
- Show customize sidebar button in footer when all sections hidden
- Add hiddenSidebarSections to store with localStorage persistence
- Rename "Display Items" to "Show" in dropdown menus
- Add 12px margin between accordion sections and bottom menu
- Add i18n keys for en-US and zh-CN

Fixes LOBE-6938

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

* 💄 style: use SlidersHorizontal icon for customize sidebar

Replace Settings2/PanelLeft icon with SlidersHorizontal to avoid
confusion with the settings gear icon.

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

* 💄 style: refine sidebar customization UX

- Move Settings entry from Footer to BottomMenu alongside Community/Resources
- Add Settings to Customize sidebar modal with eye toggle
- Allow hiding all sections (remove disabled constraint)
- Move Customize sidebar button next to help button in Footer
- Merge Agent dropdown: group Create items with Category items
- Use SlidersHorizontal icon for Customize sidebar

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

*  feat: add recents item actions and "more" drawer

- Add inline rename (same as Agent Topic) and delete to Recents items
- Topic/document/file support rename + delete, task supports delete only
- Add "more" button when items exceed pageSize, opens AllRecentsDrawer
- AllRecentsDrawer shows all cached recents from store (up to 50)
- Fetch max(pageSize, 50) items to support drawer without extra request

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

*  feat: add create agent/group modal with ChatInput and examples

- Add CreateAgentModal using base-ui Modal with ChatInputProvider
- Show suggestion examples (agent/group mode) in 2-column grid
- Submit triggers sendAsAgent/sendAsGroup to auto-generate via Agent Builder
- "Create Blank" button for skipping the prompt
- Integrate modal into AgentModalProvider for shared state across sidebar
- Wire up AddButton, NewAgentButton, and dropdown menus to open modal

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

* feat: optimitic update rename

* chore: prefetch agent detail

* feat: add recent topic meta data

* feat: add recents search

*  perf: optimize recents API with single UNION query and prefetch

- Replace 3 separate DB queries with single UNION ALL query (RecentModel)
- Add optimistic updates for rename and delete actions
- Add hover prefetch for resources (usePrefetchResource)
- Add hover prefetch for agent config on topic/task items
- Change default pageSize to 5 for both Agents and Recents
- Unify delete confirmation messages per item type

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

* chore: adjust settings page

* chore: optimize side bar

* feat: recents support right click

* chore: add pin icon to Agents

* chore: add custom side bar modal

* chore: reserve rencent drawer status

* feat: add prefetch route

* feat: add LobeAI prefetch

* fix: document and task rename and delete operation lost

* fix: group route id

* fix: lint error

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 00:13:19 +08:00
Innei cd49e98936 chore: bump lucide-react to v1.8.0 (#13703)
* chore: bump lucide-react from ^0.577.0 to ^1.8.0

 Breaking change: Github icon was removed from lucide-react v1.x (brand icons removed).
 Replaced with Github from @lobehub/icons in 5 affected files.

* fix: use GithubIcon from @lobehub/ui/icons instead of @lobehub/icons
2026-04-10 20:17:23 +08:00
Arvin Xu 7894a0a28e 🐛 fix: cascade cancel running topics when task status transitions out of running
When a task's status changes from `running` to another state (backlog/paused/completed/canceled),
automatically cancel all associated running topics and interrupt their operations.
This prevents 409 CONFLICT errors when users try to re-run a task after manually changing its status.

Fixes LOBE-6719

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:37:03 +08:00
lobehubbot d47f4fec76 🔖 chore(release): release version v2.1.49 [skip ci] 2026-04-10 09:51:03 +00:00
Tsuki 9088a074e2 🚀 release: 20260410 (#13716)
## 📦 Weekly Release 20260410

This release includes **67 commits**. Key user-facing updates below.

### New Features and Enhancements

- Introduced **Prompt Rewrite & Translate** feature for assisted input
editing.
- Added **Skill Panel** with dedicated skills tab in the skill store and
fixed skill icon rendering.
- Introduced `lh notify` CLI command for external agent callbacks.
- Added `migrate openclaw` CLI command.
- Added **GraphAgent** and `agentFactory` for graph-driven agent
execution (experimental).
- New topic auto-creation every 4 hours for long-running sessions.

### Models and Provider Expansion

- Added a new provider: **StreamLake (快手万擎)**.
- Added **GLM-5.1** model support with Kimi CodingPlan fixes.
- Added **Seedance 2.0** & **Seedance 2.0 Fast** video generation models
(pricing adjusted with 20% service fee).
- Expanded AIGC parameter support for image and video generation.
- Improved model type normalization for better provider compatibility.
- Multi-media and multiple connection mode support for ComfyUI
integration.

### Desktop Improvements

- **Embedded CLI** in the desktop app with PATH installation support.
- Added Electron version display in system tools settings.
- Fixed RuntimeConfig instant-apply working directory with recent list.
- Fixed desktop locale restore — now uses stored URL parameter instead
of system locale.
- Improved remote re-auth for batched tRPC and clean OIDC on gateway
disconnect.

### Stability, Security, and UX Fixes

- **Security**: prevented path traversal in
`TempFileManager.writeTempFile`; patched IDOR in
`addFilesToKnowledgeBase`; upgraded `better-auth` with hardened
`humanIntervention` requirement in builtin-tool-activator.
- **Context engine**: added `typeof` guard before `.trim()` calls to
prevent runtime crashes.
- **Agent runtime**: preserved reasoning state across OpenAI providers;
fixed service error serialization producing `[object Object]`; surfaced
error `reasonDetail` in `agent_runtime_end` events.
- **Knowledge Base**: cleaned up vector storage when deleting knowledge
bases.
- **Templates**: allow templates to specify `policyLoad` so default docs
are fully injected.
- **Skills**: inject current agents information when `lobehub_skill` is
activated; filter current agent out of available agents list; fix
`agents_documents` overriding `systemRole`.
- **Google Tools**: use `parametersJsonSchema` for Google tool schemas.
- **Web Crawler**: prevent happy-dom CSS parsing crash in
`htmlToMarkdown`.
- **Mobile/UI**: fixed video page icon collision, missing locale keys,
model query param; hidden LocalFile actions on topic share page; allow
manual close of hidden builtin tools.
- **Auth**: `ENABLE_MOCK_DEV_USER` now supported in `checkAuth` and
openapi auth middleware.
- **Sandbox**: stopped using `sanitizeHTMLContent` to block scripts &
sandbox styles.

### Refactors

- Library/resource tree store for hierarchy and move sync.
- Removed legacy `messageLoadingIds` from chat store.
- Removed promptfoo configs and dependencies.
- `OnboardingContextInjector` wired into context engine.

### Credits

Huge thanks to these contributors (alphabetical):

@arvinxx @canisminor1990 @cy948 @hardy-one @hezhijie0327 @Innei
@MarcellGu @ONLY-yours @rdmclin2 @rivertwilight @sxjeru @tjx666
2026-04-10 17:48:33 +08:00
Arvin Xu b95720d210 🐛 fix: add typeof guard before .trim() calls in context engine (#13715)
Add `typeof !== 'string'` checks before `.trim()` calls in BaseSystemRoleProvider,
SystemRoleInjector, and BaseProcessor to prevent TypeError when a non-string truthy
value (e.g. object, array, number) is passed at runtime.
2026-04-10 14:21:18 +08:00
Marcell Gu 560ec57f75 🐛 fix: changed builtin-tool-activator's humanIntervention to require & upgraded better auth (#13682)
* fix(builtin-tool-activator): add humanIntervention required field to activateTools manifest

- Add humanIntervention: "required" to the activateTools API manifest
- Update better-auth dependency from 1.4.6 to 1.4.9 (GHSA-xg6x-h9c9-2m83, 分数: 7.4)

* Downgrade better-auth version to 1.4.6

Thanks for your correction.
2026-04-10 14:20:51 +08:00
Arvin Xu dbca232e35 feat: support regenerateUserMessage in gateway mode (#13711)
*  feat: add gateway mode branch to regenerateUserMessage

When gateway mode is enabled, regenerateUserMessage now calls
executeGatewayAgent with parentMessageId instead of running
internal_execAgentRuntime locally. The server handles branching
and agent execution.

Fixes LOBE-6934

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

* 🐛 fix: switch branch before gateway regeneration and keep operation open

- Move switchMessageBranch before the gateway/client branch so
  activeBranchIndex is advanced and the UI shows the new response
  immediately (fixes regression from client path)
- Add onComplete callback to executeGatewayAgent so callers can
  run cleanup when the gateway session finishes
- Keep regenerate operation running until onComplete fires,
  preventing duplicate concurrent regenerations via isMessageRegenerating

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-10 13:23:19 +08:00
Zhijie He c879629439 feat: add prompt rewrite & translate feat (#13523) 2026-04-10 12:33:50 +08:00
Zhijie He 1ecf7d2be8 💄 style(image,video): extend more AIGC params support (#13597)
* 🐛 fix(image,video): preserve prompt and image when switching model

*  feat(image): smart imageUrl ↔ imageUrls conversion on model switch

- When switching from multi-image to single-image model: use imageUrls[0] as imageUrl
- When switching from single-image to multi-image model: wrap imageUrl into [imageUrl] as imageUrls
- Preserves prompt and other compatible parameters
- Add test cases for bidirectional conversion

♻️ refactor(image): simplify preserveImageInputParams logic

- Remove intermediate variables for cleaner code readability
- Condense 9 intermediate variables to 3 core ones
- Inline condition checks for simpler if statements
- Improve code clarity without changing functionality

* 🐛 fix(image): preserve imageUrl when target imageUrls default is empty array

* chore: format imageUrl & imageUrls

* feat: support imageUrls for videoGen

fix: fix ci error

fix: fix ci error

fix: fix + button

fix: fix batch images display

fix: fix muti images upload display

fix: fix ci error

style: add Seedance 2.0 support

style: add Seedance 2.0 support

fix: fix veo imageUrls logic

* style: add watermark & prompt_extend & web_search support

style: update minimax & seedream price

style: fix fix ui error

style: update z-image

style: fix video ui

style: fix seedance & seedream params

style: fix seedance & seedream params

style: fix seedance & seedream params

fix ci error

Update createImage.ts

fix ci error

fix ci error

fix ci error

fix ci error

fix ci error

fix ci error

fix: fix optimize_prompt_options

* fix rebase issue

* fix: seedance 2.0 price missing

* fix: apply some suggestions
2026-04-10 11:50:22 +08:00
LobeHub Bot 8b5aaeebdf 🌐 chore: translate non-English comments to English in comfyui (#13712)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:10:11 +08:00
CanisMinor 4787bed380 💄 style: Update agent onboarding style (#13678)
* 💄 style: Update onboarding

* style: update

* 💄 style: Update i18n

* fix: test
2026-04-10 10:44:09 +08:00
Hardy 5f25efd54c feat: add GLM-5.1 model and fix KimiCodingPlan issues (#13700)
* 🐛 fix: fix Kimi K2.5 model icon display by using deploymentName

- Change model id from 'k2p5' to 'kimi-k2.5' to match Moonshot icon keywords
- Add deploymentName 'k2p5' for API calls to use original model name
- Add KimiCodingPlan to providersWithDeploymentName list

This allows the model icon to display correctly while maintaining
backward compatibility with the API using the original 'k2p5' name.

* 🐛 fix: normalize messages for KimiCodingPlan thinking models

Add message normalization for Kimi K2.5 and K2 Thinking models to ensure
every assistant message has a thinking block when thinking is enabled.

This fixes the issue where regenerating with KimiCodingPlan after using
other providers would fail with "thinking is enabled but reasoning_content
is missing" error, because historical messages from other providers don't
have reasoning fields.

The normalization adds a placeholder thinking block when:
1. Thinking is enabled for Kimi K2.5/K2 Thinking models
2. Assistant message doesn't have reasoning content

*  feat(siliconcloud): add GLM-5.1 model support

Add GLM-5.1 (Pro) model configuration with:
- 198K context window
- Function call and reasoning capabilities
- Tiered pricing (0-32k / 32k+)
- reasoningBudgetToken32k extension parameter

* 🐛 fix: use hardcoded maxOutput mapping for KimiCodingPlan models

Replace getModelPropertyWithFallback with a simple hardcoded mapping to fix
the issue where max_tokens lookup fails when using deploymentName (k2p5).

The model id is converted to deploymentName in ChatService layer before
reaching the provider, causing getModelPropertyWithFallback('k2p5', ...) to
fail since the model card uses id 'kimi-k2.5'.

By using a hardcoded mapping that supports both model id and deploymentName,
we avoid the lookup issue while keeping the code simple (KimiCodingPlan only
has a few models).

*  test(kimiCodingPlan): add tests for thinking and max_tokens handling

Add comprehensive tests for KimiCodingPlan provider covering:
- Hardcoded maxOutput mapping for k2p5, kimi-k2.5, kimi-k2-thinking
- Thinking parameter handling for kimi-k2.5 and kimi-k2-thinking models
- Message normalization with forceThinking for assistant messages
- Tool calls with reasoning content to prevent API error

*  test(kimiCodingPlan): add tests for thinking and max_tokens handling

Add comprehensive tests for KimiCodingPlan provider covering:
- Hardcoded maxOutput mapping for k2p5, kimi-k2.5, kimi-k2-thinking
- Thinking parameter handling for kimi-k2.5 and kimi-k2-thinking models
- Message normalization with forceThinking for assistant messages
- Tool calls with reasoning content to prevent API error
2026-04-10 10:41:06 +08:00
Rylan Cai c85be1265f 🐛 fix:(agent-runtime): keep reasoning state in openai providers (#13701)
* 🐛 fix: preserve assistant reasoning in runtime state

* 🐛 fix: preserve agent reasoning and cached usage conversion

* 💬 docs: move usage retention comment to helper

* ♻️ refactor: remove redundant any cast in runtime executor

* 🐛 filter non-finite OpenAI usage values
2026-04-10 10:19:08 +08:00
Innei 4f1d2d494f feat(conversation): assistant group workflow collapse and activate-tools inspector (#13696)
* refactor(workflow): rewrite WorkflowSummary with status dot and minimal flat style

* refactor(workflow): rewrite WorkflowCollapse with unified borderless container

*  feat(workflow): add WorkflowExpandedList component and fix type errors

* ♻️ refactor(workflow): add missing Workflow components with Minimal Flat design

- WorkflowReasoningLine: cssVar tokens, aligned padding
- WorkflowToolDetail: new expandable result panel with motion animation
- WorkflowToolLine: expand chevron, getToolColor, detail panel integration
- WorkflowExpandedList: flat rendering with reasoning + tool lines

* Add tool call collapse support

Made-with: Cursor

* 💄 style(workflow): align WorkflowCollapse UI with @lobehub/ui design system

- Align border-radius, gap, padding tokens across all Workflow components
- Replace chevron expand/collapse with status icons (CheckCircle2, CircleX, Loader2)
- Use @lobehub/ui Highlighter for tool detail panel with JSON auto-formatting
- Use @lobehub/ui Flexbox for WorkflowExpandedList with proper gap and padding
- Fix delete action to use removeToolFromMessage instead of deleteAssistantMessage
- Wire debug button to existing Tool/Debug panel with full tabs
- Fix auto-collapse to only trigger on incomplete→complete transition
- Single ChevronDown with rotation for WorkflowSummary (match @lobehub/ui pattern)

* 💄 style(workflow): use AccordionItem and inspectorTextStyles for WorkflowCollapse

- Replace custom WorkflowSummary with @lobehub/ui AccordionItem
- Use StatusIndicator pattern (Block outlined 24x24) for status icon
- Apply inspectorTextStyles.root for title text (colorTextSecondary)
- Remove WorkflowSummary.tsx (dead code)
- Match Tool component AccordionItem usage (paddingBlock/Inline=4, borderless)

* 💄 style(workflow): remove divider and gap from WorkflowExpandedList

* 💄 style(workflow): align WorkflowCollapse title bar with Thinking component

* 💄 style(workflow): unify inner item spacing, font size, and colors

*  feat(workflow): add streaming scroll behavior with max-height and auto-scroll

* 💄 refactor(assistant-group): refine workflow collapse UI and duration

- Use Accordion for collapse; align tool/reasoning lines with generation state
- Show workflow header duration from summed block performance, not reasoning only

Made-with: Cursor

*  feat(inspector): enhance ActivateToolsInspector to display not found tools count

- Added localization for not found tools message in English, Chinese, and default locales.
- Updated ActivateToolsInspector to show a tooltip with the count of tools not found.
- Modified StatusIndicator to support a warning state for scenarios where no tools are activated but some are not found.

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

* 💄 style(workflow): simplify padding in WorkflowExpandedList component

- Removed unnecessary paddingInline from Flexbox elements in WorkflowExpandedList for cleaner layout.

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

*  feat(assistant-group): introduce constants and utility functions for workflow management

- Added constants for workflow timing, limits, and tool display names to enhance the assistant group's functionality.
- Implemented utility functions for processing and scoring post-tool answers, improving the workflow's response handling.
- Created new components for rendering content blocks and managing scroll behavior in the assistant group.

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

*  feat(assistant-group): enhance ContentBlock and Group components with content handling logic

- Added logic to conditionally render message content based on content availability and tool presence in ContentBlock.
- Introduced utility functions to determine substantive content and reasoning in Group, improving block partitioning for workflow management.
- Updated partitioning logic to handle trailing reasoning candidates and streamline answer and working block separation.

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

* 🙈 chore(gitignore): clarify superpowers local paths

Document that `.superpowers/` and `docs/superpowers/` are plugin/local outputs
and must not be committed.

Made-with: Cursor

* 👷 chore(ci): restore auto-tag-release workflow from canary

Revert unintended workflow edits so release tagging stays on main with
sync-main-to-canary dispatch.

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-10 02:00:38 +08:00
Innei 3b81a94d76 🐛 fix(kb): clean up vector storage when deleting knowledge bases (#13254)
* 🐛 feat(db): add findExclusiveFileIds, deleteWithFiles, deleteAllWithFiles to KnowledgeBaseModel

Add methods to safely clean up vector storage when deleting knowledge bases:
- findExclusiveFileIds: identifies files belonging only to a specific KB
- deleteWithFiles: deletes KB and its exclusive files with chunks/embeddings
- deleteAllWithFiles: bulk version for deleting all user KBs

* 🐛 fix(kb): wire vector cleanup in TRPC router, OpenAPI service, and client

- TRPC removeKnowledgeBase: use deleteWithFiles when removeFiles=true + S3 cleanup
- TRPC removeAllKnowledgeBases: use deleteAllWithFiles + S3 cleanup
- OpenAPI deleteKnowledgeBase: use deleteWithFiles + S3 cleanup
- Client service: default removeFiles=true when deleting knowledge base

* 🐛 fix(knowledgeBase): change default behavior of deleteKnowledgeBase to not remove files and update related tests

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

*  feat(knowledgeBase): add optional query parameter to deleteKnowledgeBase for file removal

- Introduced `removeFiles` query parameter to control the deletion of exclusive files and derived data when deleting a knowledge base.
- Updated `KnowledgeBaseController`, `KnowledgeBaseService`, and related schemas to support this new functionality.

This change enhances the flexibility of the delete operation, allowing users to choose whether to remove associated files.

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

* 🐛 fix: cascade knowledge base deletion and add orphan cleanup runbook

*  feat(knowledgeRepo): implement cascading deletion for file-backed documents

- Enhanced the `KnowledgeRepo` to ensure that when a document with an associated file is deleted, all related data (files, chunks, embeddings) are also removed.
- Introduced a new method `deleteDocumentWithRelations` to handle the cascading deletion logic.
- Updated tests to verify that all related entities are deleted when a file-backed document is removed.

This change improves data integrity by ensuring that no orphaned records remain after deletions.

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

* Defer DocumentService file initialization

* Fix flaky database tests and knowledge repo fixtures

* Add deletion regression tests for folders and external files

*  chore: remove kb orphan cleanup files from pr

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-10 01:56:05 +08:00
Arvin Xu a4d9967e60 🐛 fix: gateway not receiving error reasonDetail in agent_runtime_end event (#13707)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:51:19 +08:00
Arvin Xu 6a40eb8a3b 🐛 fix: resolve agent runtime service error serialization producing [object Object] (#13704)
 feat: add remote snapshot fetch for agent-tracing CLI and fix error serialization
2026-04-10 00:01:01 +08:00
Arvin Xu a23e159ef3 🔨 chore: extend execAgent with parentMessageId for Gateway regeneration/continue (#13699)
* 🌐 chore: update execServerAgentRuntime i18n copy

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

*  feat: extend execAgent with parentMessageId for regeneration/continue via Gateway

Add parentMessageId support to the execAgent API, enabling regeneration and continue-generation flows through the Gateway WebSocket path. When parentMessageId is provided, user message creation is skipped (resume mode) and the new assistant message branches from the specified parent.

Fixes LOBE-6933

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

* 🐛 fix: propagate parentMessageId through execAgents batch and fix test types

- Forward parentMessageId in execAgents executeTask to maintain batch parity with execAgent
- Fix ExecAgentResult mock types in gateway tests
- Fix messages table insert type cast in server router test

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-09 21:51:59 +08:00
Arvin Xu 1eb1fca7f2 🌐 chore: update execServerAgentRuntime i18n copy (#13698)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:52:07 +08:00
sxjeru 4100f2f700 🐛 fix: enhance model type normalization (#13548)
* feat(modelParse): enhance model type normalization and add tests for invalid types

* feat(modelParse): optimize imports and improve model type handling
2026-04-09 18:46:14 +08:00
Arvin Xu 23f91d044c 🐛 fix: buffer and deduplicate events during gateway resume (#13689)
* 🐛 fix: buffer and deduplicate events during resume to prevent out-of-order display

When reconnecting with empty lastEventId (page reload), live broadcast
events can arrive before resume replay completes, causing content to
appear out of order. Now AgentStreamClient enters resume mode: buffers
all events, waits for a 500ms gap (resume replay is dense, live events
are sparse), then deduplicates by event ID and emits in order.

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

* 🐛 fix: clear runningOperation on agent finish + resume timeout for completed sessions

- RuntimeExecutors.finish clears topic metadata.runningOperation when
  agent reaches terminal state, so stale entries don't trigger reconnect
- AgentStreamClient resume mode: add 3s timeout for empty buffer —
  if no events arrive after resume request, session has already completed,
  emit session_complete and disconnect

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

* 🐛 fix: eagerly fetch messages after topic switch to avoid skeleton flash

After switchTopic in Gateway mode, immediately fetch messages from DB
and replace in store, so the UI renders content right away instead of
showing a skeleton loading state while SWR re-fetches.

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

* 🐛 fix: eliminate skeleton flash on gateway topic switch

Match the client-mode pattern: fetch messages from DB and replaceMessages
BEFORE calling switchTopic with skipRefreshMessage: true. This ensures
messages are already in the store when the topic switches, preventing
a skeleton loading flash.

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

* 🐛 fix: flush resume buffer on session_complete before disconnect

session_complete is a top-level ServerMessage (not an agent_event), so
it bypassed the resume buffer. When it arrived during resume mode,
disconnect() cleared the buffer and all replayed events were lost.

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

* 🐛 fix: limit resume buffering to explicit reconnect scenarios only

Resume mode was triggered for ALL new connections (lastEventId always
empty on first connect), delaying live streaming for normal operations.

Now resume buffering requires explicit opt-in via resumeOnConnect option,
which is only set by reconnectToGatewayOperation (page-reload reconnect).
Normal executeGatewayAgent connections stream events immediately.

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-09 16:26:21 +08:00
LiJian 06ac87dc45 🐛 fix: should inject current agnets information when actived the lobehub_skill (#13661)
* fix: should inject current agnets information when actived the lobehub skill

* fix: not inject the agent systemRole in lobehub skill inject

* fix: should use the isLobeHubSkillActive hook to judge

* fix: change the tools inject to vars replace function

* fix: add the lost topic id & agent title

* fix: later the PlaceholderVariablesProcessor

* fix: update the description
2026-04-09 16:11:18 +08:00
Zhijie He 6d731dd116 feat: add StreamLake Provider support (#13651)
*  feat: add StreamLake (快手) support

* style: add thinking support

style: add thinking support

style: add thinking support

style: add thinking support

style: add thinking support
2026-04-09 15:00:50 +08:00
LobeHub Bot f804d0fc7c 🌐 chore: translate non-English comments to English in scripts (#13690)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:38:29 +08:00
Arvin Xu b268f44f06 🐛 fix(server): prevent path traversal in TempFileManager.writeTempFile (#13684)
🐛 fix(server): prevent path traversal in TempFileManager.writeTempFile

Use path.basename() to strip directory components from user-supplied
filenames before writing temp files, preventing arbitrary file write
via crafted filenames like "../../app/startServer.js".

Fixes LOBE-6904

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:35:20 +08:00
Rdmclin2 475622a4b9 feat: support multi media and multiple connection mode (#13624)
* test: add feishu and qq test cases

* feat: support qq websocket mode

* feat: support slack websocket mode

* feat: feishu/lark support websocket connectMode

* chore: add default connection mode

* fix: discord 401 sign error

* fix: feishu websocket need verification token

* fix: heartbeate interval

* fix: get effective connnection mode

* chore: extract  getEffectiveConnectionMode utils

* chore: merge with default settings

* chore: add connectionMode fallback

* fix: file extract error

* fix: list platforms by connectionMode

* fix: qq bot gateway

* fix: support fileList

* feat: support video list

* chore: migrate local testing to references

* chore: add bot skill

* fix: telegram file serialize error

* feat: extract file extract logic to platform client

* fix: wechat file read

* feat: skip no metion in thread and set default message mode to queue

* chore: refact download resources

* fix: feishu adapter mention and dm error

* fix: feishu thread id

* fix: slack remove action

* fix: bot resovle files
2026-04-09 14:16:03 +08:00
René Wang 7b40538486 feat: add iamge (#13688) 2026-04-09 10:33:08 +08:00
Arvin Xu 5531ff7907 🔨 chore: Gateway reconnect after page reload (#13685)
*  feat: persist runningOperation to topic metadata for gateway reconnect

- Add runningOperation field to ChatTopicMetadata type
- execAgent writes { operationId, assistantMessageId } to topic metadata
  after creating the operation
- onSessionComplete clears runningOperation from metadata (best-effort)
- Extend updateTopicMetadata tRPC schema + service to support the field

Fixes LOBE-6905

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

*  feat: add refreshGatewayToken tRPC endpoint

Signs a fresh JWT for Gateway WebSocket reconnection after page reload.
The token is scoped to the authenticated user via signUserJWT.

Fixes LOBE-6906

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

*  feat: auto-reconnect to running Gateway operation on topic load

- Add reconnectToGatewayOperation to GatewayActionImpl — refreshes JWT,
  creates local operation, and connects WebSocket with event replay
- Add useGatewayReconnect hook — checks topic metadata.runningOperation
  when entering a topic and triggers reconnection
- Wire hook into ConversationArea

Fixes LOBE-6907

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

* 🐛 fix: preserve thread scope in reconnect context and subscribe to topic metadata

- Store scope + threadId in topic metadata.runningOperation
- reconnectToGatewayOperation uses stored scope/threadId instead of
  hardcoded main/null
- useGatewayReconnect subscribes to runningOperation via useChatStore
  selector so it triggers when topic data arrives from SWR (not just
  on mount when data may be empty)

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

* 🐛 fix: update device tests to allow runningOperation metadata writes

The tests asserted updateMetadata was never called, but now execAgent
persists runningOperation. Changed to assert no device-binding metadata
was written (boundDeviceId), which is the actual intent.

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

* ♻️ refactor: use SWR for gateway reconnect lifecycle

Replace useEffect + ref with useSWR keyed by operationId. SWR
naturally deduplicates (same key = no re-fetch), handles the async
reconnect, and doesn't fire when key is null (no runningOperation).

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

* 🐛 fix: validate topic has running operation before issuing gateway token

refreshGatewayToken now requires topicId, verifies the topic belongs to
the user and has a runningOperation in metadata before signing a JWT.

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

* 💄 style: break signin title into two lines

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

* Fix signin.title formatting in auth.json

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:23:57 +08:00
Arvin Xu 4f56868545 🐛 fix: allow templates to specify policyLoad so default docs are fully injected (#13672)
* 🐛 fix: allow templates to specify policyLoad so default docs are fully injected

All documents were hardcoded to PolicyLoad.PROGRESSIVE on creation,
causing CLAW template docs (IDENTITY, SOUL, BOOTSTRAP, AGENTS) to be
progressively disclosed instead of fully injected into context.

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

* 🐛 fix: forward policyLoad through upsertDocument and persist on update

- Add policyLoad to UpsertDocumentParams and pass it through to model
- Add policyLoad param to update() so upsert's existing-document path
  writes the value instead of silently discarding it
- Ensures re-running template init migrates pre-existing docs to ALWAYS

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

* ♻️ refactor: change update() to use named params object instead of positional args

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

* ♻️ refactor: change create() and upsert() to use named params object

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

*  test: improve agentDocuments test coverage to 99%

Add tests for uncovered branches:
- normalizeLoadRule default branch (unknown rule)
- explicit 'always' rule match
- by-time-range with NaN dates
- resolveDocumentLoadPosition fallback paths
- composeToolPolicyUpdate with existing context values
- upsert create path for new filenames
- getAgentContext empty docs path

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

* 🐛 fix: preserve policyLoad when copying documents

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

*  fix: align test assertion with refactored create() params object signature

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-09 10:09:05 +08:00
Arvin Xu dc1b43d86c 🐛 fix(database): prevent IDOR in addFilesToKnowledgeBase (#13683)
🐛 fix(database): add ownership check in addFilesToKnowledgeBase to prevent IDOR

Verify that the target knowledge base belongs to the authenticated user
before inserting files, preventing unauthorized file injection into
other users' knowledge bases.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:36:51 +08:00
Arvin Xu 4d7cbfea8e 🐛 fix: skip sendMessageInServer in Gateway mode + NavItem loading fix + i18n (#13681)
* 🐛 fix: reuse existing messages in execAgent when existingMessageIds provided

When existingMessageIds contains [userMsgId, assistantMsgId], skip
creating new messages and reuse the existing ones. This fixes duplicate
messages in Gateway mode where sendMessageInServer already created
the messages before execAgentTask is called.

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

* 🐛 fix: allow clicking NavItem while loading

Loading state should only show a visual indicator, not block onClick.
This fixes topic sidebar items being unclickable during agent execution.

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

* Revert "🐛 fix: reuse existing messages in execAgent when existingMessageIds provided"

This reverts commit 43b808024d5c4a0074b692a85083a72046ab47e0.

* 🐛 fix: skip sendMessageInServer in Gateway mode to avoid duplicate messages

Gateway mode now calls execAgentTask directly instead of going through
sendMessageInServer first. The backend creates user + assistant messages
and topic in one call. executeGatewayAgent handles topic switching
internally after receiving the server response.

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

* 🌐 chore: add i18n for execServerAgentRuntime operation

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

* 🐛 fix: move temp message cleanup after executeGatewayAgent succeeds

Keep temp messages visible during the gateway call so the UI isn't
blank. On failure, mark the operation as failed instead of silently
returning — temp messages remain so the user sees something went wrong.

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

* ♻️ refactor: remove manual temp message cleanup in gateway mode

switchTopic handles new topic navigation, and fetchAndReplaceMessages
replaces the message list from DB — no need to manually delete temp
messages.

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

* 🐛 fix: clear _new key temp messages when gateway creates new topic

Pass clearNewKey: true to switchTopic so temp messages from the
optimistic create don't persist in the _new key after switching
to the server-created topic.

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

* ♻️ refactor: import ExecAgentResult from @lobechat/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-09 01:33:54 +08:00
Innei e65e2c3628 feat(desktop): embed CLI in app and PATH install (#13669)
*  feat(desktop): embed CLI in app and PATH install

Made-with: Cursor

*  feat(desktop): add CLI command execution feature and UI integration

- Implemented `runCliCommand` method in `ElectronSystemService` to execute CLI commands.
- Added `CliTestSection` component for testing CLI commands within the app.
- Updated `SystemCtr` to include CLI command execution functionality.
- Enhanced `generateCliWrapper` to create short aliases for CLI commands.
- Integrated CLI testing UI in the system tools settings page.

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

*  feat: enhance working directory handling for desktop

- Updated working directory logic to prioritize topic-level settings over agent-level.
- Introduced local storage management for agent working directories.
- Modified tests to reflect changes in working directory behavior.
- Added checks to ensure working directory retrieval is only performed on desktop environments.

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

*  feat(desktop): implement CLI command routing and cleanup

- Introduced `CliCtr` for executing CLI commands, enhancing the desktop application with CLI capabilities.
- Updated `ShellCommandCtr` to route specific commands to `CliCtr`, improving command handling.
- Removed legacy CLI path installation methods from `SystemCtr` and related services.
- Cleaned up localization files by removing obsolete entries related to CLI path installation.

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

* 🚸 settings(system-tools): show CLI embedded test only in dev mode

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-09 00:53:49 +08:00
Arvin Xu eebf9cb056 chore: add gatewayMode translations for labs (#13680)
* 🌐 i18n: add gatewayMode translations for labs

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

* Update labs.json

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:49:32 +08:00
Arvin Xu 3e7ee1fbfc 🔨 chore: integrate Gateway connection management into chat store (#13636)
*  feat: integrate Gateway connection management into chat store

Add GatewayActionImpl to aiChat slice for managing Agent Gateway
WebSocket connections per operationId. Includes connect, disconnect,
interrupt, and status tracking. Also type the execAgentTask return value.

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

*  feat: add Gateway mode branch in sendMessage for server-side agent execution

When agentGatewayUrl is set in server config (enableQueueAgentRuntime),
sendMessage now triggers server-side agent execution via execAgentTask
and receives events through the Agent Gateway WebSocket, instead of
running the agent loop client-side.

Includes:
- Expose agentGatewayUrl in GlobalServerConfig when queue mode is enabled
- Gateway event handler mapping stream events to UI message updates
- Fallback to client-side agent loop when Gateway is not configured

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

* 🐛 fix: emit disconnected event on intentional disconnect

disconnect() was only calling setStatus('disconnected') but not emitting
the 'disconnected' event. This caused the store's cleanup listener to
never fire after terminal events (agent_runtime_end), leaving stale
connections in gatewayConnections.

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

*  feat: enhance Gateway event handler for multi-step agent streaming

Support multi-step agent execution display (LLM → tool calls → next LLM)
using hybrid approach: real-time streaming for current step, DB refresh at
step transitions.

Fixes LOBE-6874

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

*  feat: wire up Gateway JWT token from execAgent to connectToGateway

Pass the RS256 JWT token returned by execAgentTask to connectToGateway
for WebSocket authentication. Also use ExecAgentResult from @lobechat/types
instead of local duplicate definition.

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

* 🐛 fix: handle wss:// protocol in AgentStreamClient buildWsUrl

When gatewayUrl already uses ws:// or wss:// protocol, use it directly
instead of stripping and re-adding the protocol prefix. Previously,
wss://host would become ws://wss://host (double protocol).

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

* 🐛 fix: queue gateway events to ensure stream_chunk waits for refreshMessages

Use a sequential Promise chain to process gateway events, so that
stream_chunk dispatches only run after stream_start's refreshMessages
resolves. Previously, chunks arrived before the new assistant message
existed in dbMessagesMap, causing updates to be silently dropped.

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

* 🐛 fix: pass operationId context to internal_dispatchMessage in gateway handler

Without operationId, internal_dispatchMessage falls back to global state
to compute the messageMapKey, which may differ from the key where
refreshMessages stored the server-created messages. Passing operationId
ensures the correct conversation context is resolved.

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

* 🐛 fix: resolve gateway streaming display issues

- Use fetchAndReplaceMessages (direct DB fetch + replaceMessages) instead
  of refreshMessages which mutates an orphaned SWR key
- Create dedicated execServerAgentRuntime operation with correct topicId
  context for internal_dispatchMessage to resolve the right messageMapKey
- Complete operation on agent_runtime_end instead of relying on
  onSessionComplete callback
- Keep loading state active between steps (only clear on agent_runtime_end)
  so users don't think the session ended during tool execution gaps

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

* 🐛 fix: maintain loading state across gateway step transitions

- Create dedicated execServerAgentRuntime operation with correct topicId
- Use fetchAndReplaceMessages instead of orphaned refreshMessages SWR key
- Re-apply loading after tool_end refresh so UI stays active between steps
- Complete operation on agent_runtime_end
- Add record-app-screen.sh for automated screen recording
- Output recordings to .records/ (gitignored)

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

* 🐛 fix: show loading on assistant message immediately in stream_start

Set loading on the current assistant message BEFORE awaiting
fetchAndReplaceMessages, so the UI shows a loading indicator while
waiting for the DB response instead of appearing frozen.

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

* 🐛 fix: drive gateway loading state via operation system instead of messageLoadingIds

Associate the assistant message with the gateway operation via
associateMessageWithOperation so the Conversation store's operation-based
loading detection (isGenerating) works correctly. This shows the proper
loading skeleton on the assistant message while waiting for gateway events.

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

* ♻️ refactor: remove unused internal_toggleMessageLoading from gateway handler

Loading state is now fully driven by the operation system via
associateMessageWithOperation + completeOperation. The old
messageLoadingIds-based approach is no longer needed.

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

* 🐛 fix: rewrite record-app-screen.sh to use CDP screenshot assembly

Replace broken ffmpeg avfoundation live recording (corrupts on kill) with
agent-browser CDP screenshot capture + ffmpeg assembly on stop. This works
reliably on any screen including external monitors.

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

*  feat: add Gateway Mode lab toggle and fix CI type error

- Add enableGatewayMode to UserLabSchema as experimental feature
- Add lab selector and settings UI toggle in Advanced > Labs
- Gateway mode now requires both server config (agentGatewayUrl) AND
  user opt-in via Labs toggle
- Fix TS2322: result.token (string | undefined) → fallback to ''
- Add i18n keys for gateway mode feature

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

*  feat: hide Gateway Mode toggle when agentGatewayUrl is not configured

Only show the lab toggle when the server has AGENT_GATEWAY_URL set,
so users without gateway infrastructure don't see the option.

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

* 💄 style: move Gateway Mode toggle below Input Markdown in labs section

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

* 🐛 fix: remove default AGENT_GATEWAY_URL value and make schema optional

Without an explicit env var, the gateway URL should be undefined so the
lab toggle and gateway mode are not available.

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

* 📝 docs: update SKILL.md to reference record-app-screen.sh

Replace outdated record-gateway-demo.sh references with the renamed
record-app-screen.sh and its start/stop lifecycle documentation.

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

* 📝 docs: add record-app-screen reference doc and slim down SKILL.md

Move detailed recording documentation to references/record-app-screen.md
and keep SKILL.md concise with a link to the full reference.

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

* 🐛 fix: guard GatewayStreamNotifier with AGENT_GATEWAY_URL check

AGENT_GATEWAY_URL is now optional, so check both URL and service token
before wrapping with GatewayStreamNotifier to avoid TS2345.

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

* ♻️ refactor: extract gateway execution logic to GatewayActionImpl

Move server-side gateway execution logic from conversationLifecycle.ts
into GatewayActionImpl.startGatewayExecution(). The sendMessage flow
now does a simple early return when gateway mode is active, keeping
the existing client-mode code path untouched.

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

* ♻️ refactor: split gateway into isGatewayModeEnabled check + executeGatewayAgent

Replace fire-and-forget startGatewayExecution with explicit check/execute
pattern. Caller does: if (check) { await execute(); return; } — giving
proper error handling and clearer control flow.

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-08 23:31:26 +08:00
renovate[bot] 84eff30be1 Update dependency lucide-react to ^0.577.0 (#13580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 23:31:20 +08:00
Innei 50a1cc1ec2 ♻️ refactor(resource): tree store for library hierarchy and move sync (#13640)
*  feat(ResourceManager): integrate tree store for folder management and enhance file operations

- Added `useTreeStore` to manage folder structure and state, replacing previous file store dependencies.
- Updated `EmptyPlaceholder` to utilize `currentFolderId` for file uploads.
- Refactored `MoveToFolderModal` to use tree store for moving items, improving folder navigation.
- Enhanced drag-and-drop functionality in `DndContextWrapper` to support moving items between folders.
- Removed obsolete `LibraryHierarchy` state management, streamlining folder operations.
- Improved file renaming and deletion processes to ensure tree state consistency.

This update enhances the overall file management experience by leveraging a dedicated tree store for better performance and maintainability.

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

*  feat(TreeAction): enhance resource movement and update handling

- Updated mutation logic for moving resources to differentiate between items visible in the Explorer and those not visible, improving performance and user experience.
- Added refresh functionality for the file list after resource updates (move, update, delete) to ensure the Explorer reflects the latest state.
- Refactored mutation methods to use async/await for better readability and error handling.

This update streamlines resource management within the tree structure, ensuring a more responsive and consistent user interface.

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

* Fix file updates and tree move fallback regressions

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-08 23:26:58 +08:00
Arvin Xu d49aba748e 🐛 fix: hide LocalFile actions in topic share page (#12254)
🐛 fix: hide LocalFile actions (Open/Show in Folder) in share page

In topic share pages, the LocalFile component was showing 'Open' and
'Show in Folder' action buttons on hover, which are desktop-only
operations not available to share page viewers.

- Add 'readonly' prop to LocalFile component to disable interactive actions
- Detect share page context via topicShareId in LocalFile Render plugin
- Skip Popover rendering when readonly is true
2026-04-08 22:45:08 +08:00
Arvin Xu 8a0c3cb36a ♻️ refactor: remove legacy messageLoadingIds from chat store (#13662)
* ♻️ refactor: remove legacy messageLoadingIds from chat store

The messageLoadingIds state and internal_toggleMessageLoading action in the
chat store have been fully superseded by the operation system. The state was
being written to but never read by any consumer — all UI components and
selectors already use operation-based selectors (isMessageGenerating,
isMessageProcessing, etc.).

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

* 📝 chore: update skill docs to remove messageLoadingIds references

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

* 🐛 fix: replace messageLoadingIds with operationSelectors in generation action

The Conversation store's regenerateUserMessage was reading messageLoadingIds
from the chat store to check if a message is already being processed. Replace
with operationSelectors.isMessageProcessing which is the correct way to check
operation state.

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

* 🐛 fix: add operationsByMessage to test mocks for operation selector

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-08 21:54:11 +08:00
LiJian 26d1d6bbfb 🐛 fix: slove the agents_documents will coverd the systemRole (#13667)
fix: slove the agents_documents will coverd the systemRole
2026-04-08 20:54:20 +08:00
YuTengjing c5ec0ef2a1 💰 chore: adjust Seedance 2.0 pricing with 20% service fee (#13676) 2026-04-08 20:50:18 +08:00
YuTengjing 6d0c8d710a 🐛 fix: video page icon collision, missing locale keys, and model query param (#13671) 2026-04-08 19:44:35 +08:00
Rdmclin2 e10265fadd feat: add skill panel and fix skill icon (#13666)
* fix: custom agent skill icon

* feat: support skill detail

* chore: remove unnecessary custom tag
2026-04-08 18:51:01 +08:00
Arvin Xu c68dfa00df feat(cli): add lh notify command for external agent callbacks (#13664)
*  feat(cli): add `lh notify` command for external agent callbacks

Add a new `lh notify` CLI command and server-side TRPC endpoint that allows
external agents (e.g. Claude Code) to send callback messages to a topic and
trigger the agent loop to process them.

Fixes LOBE-6888

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

* 🔧 chore(cli): replace sessionId with agentId and threadId in notify command

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-08 18:03:55 +08:00
lobehubbot 7d599a980f 🔖 chore(release): release version v2.1.48 [skip ci] 2026-04-07 14:50:49 +00:00
2158 changed files with 142561 additions and 18526 deletions
+298
View File
@@ -0,0 +1,298 @@
---
name: bot
description: 'Bot platform architecture (Discord, Slack, Telegram, Feishu/Lark, QQ, WeChat). Use when working on inbound webhooks, Chat SDK message routing, agent execution from chat platforms, queue-mode callbacks, gateway lifecycle (websocket/polling), bot provider CRUD/credentials, or platform-specific clients/adapters/schemas. Triggers on bot, channel, webhook, mention, Chat SDK, agent bot provider, gateway, bot-callback, qstash bot.'
---
# Bot System
> **Last updated: 2026-04-08.** Implementation evolves quickly — this doc is a map, not the source of truth. Always read the key files below to verify behavior, especially per-platform quirks. Update this doc when the architecture changes.
LobeChat agents can answer inside external chat platforms. Inbound messages flow through the Chat SDK (`chat` npm package), get routed to the right agent by `(platform, applicationId)`, executed via `AiAgentService`, and replied back through a per-platform `PlatformClient`. There are **two execution modes** (in-memory vs queue/QStash) and **three connection modes** (`webhook`, `websocket`, `polling`).
## Supported Platforms
| Platform | id | Default mode | Markdown | Edit | Notes |
| -------- | ---------- | ------------------------------- | ----------------- | ------ | -------------------------------------------------------------------------------------- |
| Discord | `discord` | `websocket` | yes | yes | Persistent gateway via Chat SDK adapter; reaction-thread quirks; native slash commands |
| Slack | `slack` | `websocket` (Socket Mode) | yes (mrkdwn) | yes | Multi-mode — user can pick `webhook` per provider |
| Telegram | `telegram` | `webhook` | yes (HTML) | yes | `setMyCommands` menu via `registerBotCommands` |
| Feishu | `feishu` | `websocket` (Lark SDK WSClient) | **no** (stripped) | yes | Multi-mode; shared client with Lark |
| Lark | `lark` | `websocket` | **no** | yes | Same client/schema as Feishu, different domain |
| QQ | `qq` | `websocket` | **no** | **no** | All replies are final-only |
| WeChat | `wechat` | `polling` (iLink long-poll) | **no** | **no** | 10-minute gateway window |
`supportsMarkdown=false` ⇒ outbound markdown is stripped to plain text via `stripMarkdown` and the AI is told not to use markdown. `supportsMessageEdit=false` ⇒ no progress edits — only the final reply is sent.
**Multi-mode connection** — Slack/Feishu/Lark/QQ ship as websocket but support `webhook` per-provider via `settings.connectionMode`. The runtime always merges schema defaults into stored settings before resolving the mode (`resolveBotProviderConfig` / `resolveConnectionMode` in `platforms/utils.ts`), so the schema's `field.default` is the source of truth — set it correctly when adding a new multi-mode platform.
## Inbound Flow (one webhook → reply)
```
Platform server
│ POST /api/agent/webhooks/[platform]/[appId]
route.ts ── catch-all `[[...appId]]` route
BotMessageRouter (singleton)
│ • lazy-loads bot per `platform:applicationId`
│ • merges schema defaults + provider.settings (mergeWithDefaults)
│ • builds Chat SDK Chat<any> with createIoRedisState (if Redis available)
│ • registerHandlers: onNewMention / onSubscribedMessage / onNewMessage(/.dm)
│ • registerCommands: /new (reset topic), /stop (interrupt)
chatBot.webhooks[platform](req) ← Chat SDK parses → fires events
AgentBridgeService.handleMention / handleSubscribedMessage
│ • activeThreads guard (no duplicate runs per thread)
│ • adds 👀 reaction (eyes), startTyping
│ • merges debounced/queued skipped messages (mergeSkippedMessages)
│ • extractFiles (buffer → fetchData → url)
│ • formatPrompt (sanitize mention + speaker tag + referenced_message)
├── In-memory mode ──► AiAgentService.execAgent({ stepCallbacks })
│ → onAfterStep edits progress message live
│ → onComplete edits final reply, splits via splitMessage(charLimit)
└── Queue mode (isQueueAgentRuntimeEnabled) ──► execAgent({ stepWebhook, completionWebhook, webhookDelivery: 'qstash' })
→ returns immediately, callbacks land at /api/agent/webhooks/bot-callback
```
The router caches loaded bots in memory. Cache is **invalidated** by `BotMessageRouter.invalidateBot(platform, appId)` whenever the TRPC `update`/`delete` mutations run, so new credentials/settings take effect on the next webhook.
## Execution Modes
### In-memory (default)
`AgentBridgeService.executeWithInMemoryCallbacks` wraps `execAgent` with `stepCallbacks`. Lives in one process — Promise-based wait, 30-min timeout, edits the same `progressMessage` after every step. Topic title is summarized inline via `SystemAgentService`.
### Queue (`isQueueAgentRuntimeEnabled`)
`AgentBridgeService.executeWithWebhooks`:
1. Posts the `renderStart` placeholder, captures `progressMessageId`.
2. Calls `execAgent` with `stepWebhook` and `completionWebhook` pointing at `${INTERNAL_APP_URL ?? APP_URL}/api/agent/webhooks/bot-callback`, plus `webhookDelivery: 'qstash'`.
3. Returns immediately; the bridge `finally` block keeps the active-thread marker held until the `completion` callback fires.
`/api/agent/webhooks/bot-callback/route.ts` verifies the QStash signature and hands off to `BotCallbackService.handleCallback`:
- `type: 'step'``handleStep` re-renders `renderStepProgress`, edits `progressMessageId` (skipped if `displayToolCalls=false` or platform `supportsMessageEdit=false`).
- `type: 'completion'``handleCompletion` writes the final reply (or error/interrupted message), removes the 👀 reaction, clears active-thread tracker, fires async `summarizeTopicTitle`.
`BotCallbackService.createMessenger` reloads provider + credentials from DB and rebuilds a `PlatformClient` per call (no in-memory state).
## Commands
Defined in `BotMessageRouter.buildCommands` and registered via two paths:
- **Native slash commands** (Slack/Discord): `bot.onSlashCommand('/<name>', ...)`
- **Text-based fallback** (Telegram/Feishu/QQ/Lark/WeChat): `bot.onNewMessage(/^\/(new|stop)(\s|$|@)/, ...)` plus a per-mention `tryDispatch` so commands work even before subscribe.
Built-in commands:
- `/new` — clears `topicId` in thread state, next message starts a fresh topic.
- `/stop` — interrupts the active execution (calls `AiAgentService.interruptTask` if `operationId` is known; otherwise queues a deferred stop via `requestStop`/`pendingStopThreads`, also aborts the startup phase via `startupControllers`).
To add a command, append to `buildCommands` — it auto-registers everywhere; on Telegram it also surfaces in the `/` menu via `client.registerBotCommands``setMyCommands`.
## Active-thread State (statics on `AgentBridgeService`)
- `activeThreads: Set<threadId>` — prevents duplicate runs per thread (must guard before stale-topic check, otherwise concurrent messages can drop).
- `activeOperations: Map<threadId, operationId>` — needed by `/stop` once `execAgent` returns.
- `startupControllers: Map<threadId, AbortController>` — cancels pre-`operationId` work (topic/tool prep).
- `pendingStopThreads: Set<threadId>``/stop` arrived before `operationId` existed; consumed once available.
In **queue mode**, the bridge `finally` skips cleanup so the marker persists until `BotCallbackService.handleCompletion` calls `clearActiveThread`.
## Topic Lifecycle in Threads
- `handleMention` always treats the message as the start of a new conversation.
- `handleSubscribedMessage` reads `topicId` from `thread.state`. If the topic is stale (`> 4 hours` since `updatedAt`), state is cleared and it retries as a fresh mention.
- If `execAgent` fails with a Postgres FK violation on `topic_id` (cached topic was deleted), the bridge clears state and retries as a mention.
- `subscribe()` is gated by `client.shouldSubscribe(threadId)` — Discord top-level channels return `false` so we don't follow up there.
## Attachments
`AgentBridgeService.extractFiles` resolves attachments in priority order:
1. `att.buffer` — already downloaded by the adapter (WeChat/Feishu inbound).
2. `att.fetchData()` — adapter-provided lazy download with auth (Telegram, Slack, Feishu history). **Required** when URLs are token-protected — naive `fetch(url)` later in `ingestAttachment.ts` has no credentials.
3. `att.url` — public CDN fallback (Discord, public QQ).
`inferMimeType` / `inferName` patch Telegram-style `photo` payloads (no `mimeType`/`name` from Bot API → defaults to `image/jpeg`) so vision models actually see them. Quoted-message attachments are also pulled from `raw.referenced_message.attachments` (Discord).
## Concurrency
`settings.concurrency` is `'queue'` or `'debounce'`:
- `debounce` → Chat SDK debounces inbound messages by `debounceMs`; `mergeSkippedMessages` joins skipped texts/attachments into the current message before handing to the agent.
- `queue` → Chat SDK serializes per-thread; the bridge's own `activeThreads` set is still required because in queue mode the SDK lock releases before the agent finishes.
## Gateway (persistent platforms)
Webhook platforms run fine in serverless functions. Persistent platforms (`websocket`, `polling`) need a long-running listener — that's the **gateway**.
**`GatewayService.startClient(platform, appId, userId)`** (`src/server/services/gateway/index.ts`):
- On Vercel + persistent mode → `BotConnectQueue.push` (Redis hash) and mark runtime status `queued`. The cron picks it up.
- On Vercel + webhook mode → start the client inline (one HTTP call).
- Off-Vercel → `GatewayManager` singleton holds long-lived clients in process.
**`GET /api/agent/gateway/route.ts`** (cron, `Bearer ${CRON_SECRET}`):
- Iterates registered platforms and starts every enabled persistent provider with `durationMs = 10min`, then in `after(...)` polls `BotConnectQueue` every 30s for new connect requests, until the window expires.
- `getEffectiveConnectionMode(platform, settings)` is the only place that resolves per-provider mode — respect it everywhere.
**`POST /api/agent/gateway/start/route.ts`** is the non-Vercel `ensureRunning` entry point (`Bearer ${KEY_VAULTS_SECRET}`).
**Runtime status** is stored in Redis at `bot:runtime-status:platform:appId` with TTL ≈ `durationMs + 60s`. States: `starting | connected | disconnected | failed | queued`. Updated by each `PlatformClient.start/stop` and by the gateway service.
## Platform Definitions
Each platform exposes a `PlatformDefinition` registered in `platforms/index.ts`:
```ts
{
id: 'discord',
name: 'Discord',
connectionMode: 'websocket', // recommended default
schema: FieldSchema[], // applicationId + credentials + settings
clientFactory: new DiscordClientFactory(),
supportsMarkdown?: boolean, // default true
supportsMessageEdit?: boolean, // default true
documentation?: { portalUrl, setupGuideUrl },
}
```
`schema` drives both server validation (`mergeWithDefaults`, `extractDefaults`) **and** the auto-generated UI form. Top-level keys `applicationId` / `credentials` / `settings` map to DB columns. Common settings fields live in `platforms/const.ts` (`displayToolCallsField`, `serverIdField`, `userIdField`).
Each platform implements `PlatformClient` (see `platforms/types.ts`):
- Lifecycle: `start(opts?)`, `stop()`
- Inbound: `createAdapter()` → Chat SDK adapter map
- Outbound: `getMessenger(platformThreadId)``{ createMessage, editMessage, removeReaction, triggerTyping, updateThreadName? }`
- Formatting: `formatMarkdown?`, `formatReply?` (usage-stats footer when `showUsageStats`)
- Helpers: `extractChatId`, `parseMessageId`, `sanitizeUserInput`, `shouldSubscribe`, `resolveReactionThreadId`
- Optional patches: `applyChatPatches(chatBot)` (Discord uses this for `forwardedInteractions` + `threadRecovery`)
- Optional menu: `registerBotCommands(commands)` (Telegram `setMyCommands`)
`ClientFactory.validateCredentials` is called from the TRPC `testConnection` mutation — implement it to hit the platform API and return useful per-field errors.
## Database
**Schema** (`packages/database/src/schemas/agentBotProvider.ts`):
```ts
agent_bot_providers (
id uuid pk,
agent_id text fk agents.id (cascade),
user_id text fk users.id (cascade),
platform varchar(50), // 'discord' | 'slack' | …
application_id varchar(255),
credentials text, // KeyVaults-encrypted JSON
settings jsonb default '{}',
enabled boolean default true,
timestamps
)
unique (platform, application_id)
```
**Model** (`packages/database/src/models/agentBotProvider.ts`):
- User-scoped: `create / update / delete / query / findById / findByAgentId / findEnabledByApplicationId`. Credentials are encrypted/decrypted via the injected `KeyVaultsGateKeeper`.
- Static (system-wide): `findByPlatformAndAppId`, `findEnabledByPlatform` — used by webhook routing & gateway sync, since they don't have a user context yet.
**TRPC router** (`src/server/routers/lambda/agentBotProvider.ts`):
| Procedure | Notes | |
| -------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------ |
| `listPlatforms` | Returns `SerializedPlatformDefinition[]` (no `clientFactory`) | |
| `create` / `update` / `delete` | Calls `BotMessageRouter.invalidateBot` + `GatewayService.stopClient` so changes take effect | |
| `list` / `getByAgentId` / `getRuntimeStatus` | Decorate rows with Redis runtime status | |
| `connectBot` | Returns \`{ status: 'started' | 'queued' }\` |
| `testConnection` | Calls `clientFactory.validateCredentials` | |
| `wechatGetQrCode` / `wechatPollQrStatus` | iLink onboarding flow | |
Client service: `src/services/agentBotProvider.ts`. Store actions: `src/store/agent/slices/bot/action.ts`. UI: `src/routes/(main)/agent/channel/{list,detail}` — settings form is auto-generated from each platform's `schema`.
## Reply Templates
`src/server/services/bot/replyTemplate.ts` exports `renderStart`, `renderStepProgress`, `renderFinalReply`, `renderError`, `renderStopped`, `splitMessage`. Step progress carries elapsed time, last LLM content, last tools, totals; final reply uses `client.formatMarkdown` then `client.formatReply` (which optionally appends `formatUsageStats`). `splitMessage(text, charLimit)` chunks at paragraph → line → hard cut.
`src/server/services/bot/ackPhrases/` provides randomized ack phrases.
## Key Files
```plaintext
Webhook routes:
src/app/(backend)/api/agent/webhooks/[platform]/[[...appId]]/route.ts — inbound catch-all
src/app/(backend)/api/agent/webhooks/bot-callback/route.ts — qstash bot callback
src/app/(backend)/api/agent/gateway/route.ts — cron gateway (10min window)
src/app/(backend)/api/agent/gateway/start/route.ts — non-Vercel ensureRunning
Bot service:
src/server/services/bot/index.ts — barrel
src/server/services/bot/BotMessageRouter.ts — lazy bot loading + handler registration + commands
src/server/services/bot/AgentBridgeService.ts — Chat SDK ↔ AiAgentService bridge, both exec modes
src/server/services/bot/BotCallbackService.ts — qstash callback handler
src/server/services/bot/formatPrompt.ts — speaker tag + referenced_message + sanitize
src/server/services/bot/replyTemplate.ts — render*/splitMessage
src/server/services/bot/ackPhrases/ — randomized acks
src/server/services/bot/__tests__/ — unit tests for the above
Platform abstraction:
src/server/services/bot/platforms/index.ts — registry singleton + exports
src/server/services/bot/platforms/types.ts — PlatformClient/Definition/FieldSchema/ClientFactory
src/server/services/bot/platforms/registry.ts — PlatformRegistry class
src/server/services/bot/platforms/utils.ts — mergeWithDefaults, getEffectiveConnectionMode, formatUsageStats, runtimeKey
src/server/services/bot/platforms/const.ts — shared FieldSchema fragments (displayToolCalls, serverId, userId)
src/server/services/bot/platforms/stripMarkdown.ts — used by no-markdown platforms
Per-platform (each ships definition.ts, schema.ts, client.ts, const.ts, protocol-spec.md):
src/server/services/bot/platforms/discord/ — websocket gateway + chat patches
src/server/services/bot/platforms/slack/ — multi-mode (Socket Mode / webhook), markdownToMrkdwn
src/server/services/bot/platforms/telegram/ — webhook, markdownToHTML, registerBotCommands
src/server/services/bot/platforms/feishu/ — feishu + lark share client/schema (definitions/{feishu,lark,shared}.ts)
src/server/services/bot/platforms/qq/ — websocket, no markdown, no edit
src/server/services/bot/platforms/wechat/ — long-poll, no markdown, no edit
Gateway:
src/server/services/gateway/index.ts — GatewayService (Vercel-aware startClient/stopClient)
src/server/services/gateway/GatewayManager.ts — long-running client registry (non-Vercel)
src/server/services/gateway/botConnectQueue.ts — Redis hash queue with TTL
src/server/services/gateway/runtimeStatus.ts — Redis bot:runtime-status keys
Database:
packages/database/src/schemas/agentBotProvider.ts — agent_bot_providers table
packages/database/src/models/agentBotProvider.ts — encrypted CRUD + system-wide finders
TRPC + client:
src/server/routers/lambda/agentBotProvider.ts — TRPC router
src/services/agentBotProvider.ts — client wrapper
src/store/agent/slices/bot/action.ts — Zustand actions
UI:
src/routes/(main)/agent/channel/list.tsx — channel list
src/routes/(main)/agent/channel/detail/ — auto-generated form (Header/Body/Footer)
src/routes/(main)/agent/channel/const.ts — platform icons
Types & runtime status:
src/types/botRuntimeStatus.ts — BOT_RUNTIME_STATUSES enum + snapshot type
```
## Adding a New Platform
1. Create `src/server/services/bot/platforms/<id>/`:
- `definition.ts``PlatformDefinition` registered in `platforms/index.ts`
- `schema.ts``FieldSchema[]` (`applicationId` + `credentials` + `settings`); reuse fragments from `../const.ts`
- `client.ts``class XClientFactory extends ClientFactory` returning a `PlatformClient` (lifecycle + adapter + messenger + helpers)
- `const.ts``DEFAULT_X_CONNECTION_MODE`, history limits, etc.
- `protocol-spec.md` — protocol notes (every existing platform has one)
2. Pick the right `connectionMode` — webhook is much simpler if the platform supports it.
3. If the platform can't render markdown, set `supportsMarkdown: false` and implement `formatMarkdown` via `stripMarkdown`.
4. If it can't edit messages, set `supportsMessageEdit: false``BotCallbackService` will skip step edits and only send the final reply.
5. Implement `validateCredentials` so the UI's "Test connection" button gives useful errors.
6. Add the platform icon in `src/routes/(main)/agent/channel/const.ts` and register the platform in `src/server/services/bot/platforms/index.ts`.
7. Add i18n keys under `channel.*` in `src/locales/default/setting.ts` (or wherever the channel namespace lives) — the schema's `label`/`description`/`placeholder`/`enumLabels` are i18n keys.
+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
+1 -1
View File
@@ -5,7 +5,7 @@ description: Internationalization guide using react-i18next. Use when adding tra
# LobeHub Internationalization Guide
- Default language: Chinese (zh-CN)
- Default language: English (en-US)
- Framework: react-i18next
- **Only edit files in `src/locales/default/`** - Never edit JSON files in `locales/`
- Run `pnpm i18n` to generate translations (or manually translate zh-CN/en-US for dev preview)
+57
View File
@@ -30,6 +30,63 @@ This is NON-NEGOTIABLE. Skipping Linear comments is a workflow violation.
When creating issues with `mcp__linear-server__create_issue`, **MUST add the `claude code` label**.
## Creating Sub-issue Trees
When breaking a parent issue into a tree of sub-issues (e.g., task decomposition for LOBE-xxx), follow these rules — they work around real limitations of the Linear MCP tools.
### 1. ALWAYS prefix titles with an ordering index
The Linear Sub-issues panel displays children by `sortOrder`, which **defaults to newest-first** (most recently created appears on top). Neither parallel nor serial creation will produce the intended top-to-bottom reading order, and the MCP `save_issue` tool does **not expose a `sortOrder` parameter** — you cannot set order at create time.
**Workaround**: encode execution order in the title itself:
```plaintext
[1] [db] add schema fields
[2] [db] new table + repository
[3] [service] business logic layer
[4] [api] REST endpoints
[4.1] [sdk] client SDK wrapper
[4.1.1] [app] consumer integration
[4.1.2] [app] UI surface
[4.2] [ui] dashboard page
```
Even when the panel shuffles, the reader can mentally reconstruct the dependency graph at a glance. Dotted numbering `[n.m.k]` should mirror the parent-child nesting so the index and the tree agree.
### 2. Nest sub-issues by logical parent-child, not flat under the root
Linear supports **unlimited sub-issue depth**. A flat list of 8+ siblings under one root is hard to scan. Group by main-subordinate logic:
- Core service → its SDK → SDK consumers
- Don't create a sibling when a child is more accurate
Use `parentId: "LOBE-xxxx"` at creation (or `save_issue` to move). Moving an issue's parent does not disturb its `blockedBy` relations.
### 3. Sub-issue creation order is dictated by `blockedBy`
`blockedBy` requires the blocker to exist first (you need its LOBE-id). So:
1. **Topologically sort** the DAG — leaves (no deps) first, roots last
2. Create issues with zero deps in the first wave
3. Create dependent issues only after collecting the blocker IDs from prior responses
4. `blockedBy` is **append-only**; passing it again does not overwrite — safe to re-run
### 4. Don't waste rounds trying to parallelize
MCP tool calls in a single message look parallel but execute sequentially on the server, and you still need blocker IDs from earlier responses. Just issue calls in dependency order; optimizing for parallelism gains nothing here.
### 5. Keep each sub-issue description self-contained
Each sub-issue should state:
- Goal (12 lines)
- Key files to touch
- Concrete changes / acceptance criteria
- Dependencies (link to blocker issues by `LOBE-xxxx`)
- Validation steps
The implementer may open only the sub-issue, not the parent — don't rely on context that lives only in the parent description.
## Completion Comment Format
Every completed issue MUST have a comment summarizing work done:
+30 -635
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
@@ -379,621 +383,30 @@ agent-browser --auto-connect snapshot -i
# Part 2: osascript (Native macOS App Bot Testing)
Use AppleScript via `osascript` to control native macOS desktop apps for bot testing. This works with any app that supports macOS Accessibility, without needing CDP or Chromium.
Use AppleScript via `osascript` to control native macOS desktop apps for bot testing. Works with any app that supports macOS Accessibility, no CDP or Chromium needed.
## Core osascript Patterns
The pattern is the same for every platform:
### Activate an App
1. **Activate** the app (`tell application "X" to activate`)
2. **Navigate** to a channel/chat (Quick Switcher `Cmd+K` or Search `Cmd+F`)
3. **Send** a message (clipboard paste `Cmd+V` + Enter)
4. **Wait** for the bot response
5. **Screenshot** for verification (`screencapture` + `Read` tool)
```bash
osascript -e 'tell application "Discord" to activate'
```
## Per-Platform References
### Type Text
Pick the file for your target platform — each contains activation, navigation, send-message, and verification snippets specific to that app:
```bash
# Type character by character (reliable, but slow for long text)
osascript -e 'tell application "System Events" to keystroke "Hello world"'
| 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` |
# Press Enter
osascript -e 'tell application "System Events" to key code 36'
# Press Tab
osascript -e 'tell application "System Events" to key code 48'
# Press Escape
osascript -e 'tell application "System Events" to key code 53'
```
### Paste from Clipboard (fast, for long text)
```bash
# Set clipboard and paste — much faster than keystroke for long messages
osascript -e 'set the clipboard to "Your long message here"'
osascript -e 'tell application "System Events" to keystroke "v" using command down'
```
Or in one shot:
```bash
osascript -e '
set the clipboard to "Your long message here"
tell application "System Events" to keystroke "v" using command down
'
```
### Keyboard Shortcuts
```bash
# Cmd+K (quick switcher in Discord/Slack)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
# Cmd+F (search)
osascript -e 'tell application "System Events" to keystroke "f" using command down'
# Cmd+N (new message/chat)
osascript -e 'tell application "System Events" to keystroke "n" using command down'
# Cmd+Shift+K (example: multi-modifier)
osascript -e 'tell application "System Events" to keystroke "k" using {command down, shift down}'
```
### Click at Position
```bash
# Click at absolute screen coordinates
osascript -e '
tell application "System Events"
click at {500, 300}
end tell
'
```
### Get Window Info
```bash
# Get window position and size
osascript -e '
tell application "System Events"
tell process "Discord"
get {position, size} of window 1
end tell
end tell
'
```
### Screenshot
```bash
# Full screen
screencapture /tmp/screenshot.png
# Interactive region select
screencapture -i /tmp/screenshot.png
# Specific window (by window ID from CGWindowList)
screencapture -l < WINDOW_ID > /tmp/screenshot.png
```
To get window ID for a specific app:
```bash
osascript -e '
tell application "System Events"
tell process "Discord"
get id of window 1
end tell
end tell
'
```
### Read Accessibility Elements
```bash
# Get all UI elements of the frontmost window (can be slow/large)
osascript -e '
tell application "System Events"
tell process "Discord"
entire contents of window 1
end tell
end tell
'
# Get a specific element's value
osascript -e '
tell application "System Events"
tell process "Discord"
get value of text field 1 of window 1
end tell
end tell
'
```
> **Warning:** `entire contents` can be extremely slow on complex UIs. Prefer screenshots + `Read` tool for visual verification.
### Read Screen Text via Clipboard
For reading the latest message or response from an app:
```bash
# Select all text in the focused area and copy
osascript -e '
tell application "System Events"
keystroke "a" using command down
keystroke "c" using command down
end tell
'
sleep 0.5
# Read clipboard
pbpaste
```
---
## Client: Discord
**App name:** `Discord` | **Process name:** `Discord`
### Activate & Navigate
```bash
# Activate Discord
osascript -e 'tell application "Discord" to activate'
sleep 1
# Open Quick Switcher (Cmd+K) to navigate to a channel
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
### Send Message to Bot
```bash
# The message input is focused after navigating to a channel
# Type a message
osascript -e 'tell application "System Events" to keystroke "/hello"'
sleep 0.5
osascript -e 'tell application "System Events" to key code 36' # Enter
```
### Send Long Message (via clipboard)
```bash
osascript -e '
tell application "Discord" to activate
delay 0.5
set the clipboard to "Write a 3000 word essay about space exploration"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
### Verify Bot Response
```bash
# Wait for bot to respond, then screenshot
sleep 10
screencapture /tmp/discord-bot-response.png
# Read with the Read tool for visual verification
```
### Full Bot Test Example
```bash
#!/usr/bin/env bash
# test-discord-bot.sh — Send message and verify bot response
# 1. Activate Discord and navigate to channel
osascript -e '
tell application "Discord" to activate
delay 1
-- Quick Switcher
tell application "System Events" to keystroke "k" using command down
delay 0.5
tell application "System Events" to keystroke "bot-testing"
delay 1
tell application "System Events" to key code 36
delay 2
'
# 2. Send test message
osascript -e '
set the clipboard to "!ping"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 3. Wait for response and capture
sleep 5
screencapture /tmp/discord-test-result.png
echo "Screenshot saved to /tmp/discord-test-result.png"
```
---
## Client: Slack
**App name:** `Slack` | **Process name:** `Slack`
### Activate & Navigate
```bash
# Activate Slack
osascript -e 'tell application "Slack" to activate'
sleep 1
# Quick Switcher (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
### Send Message to Bot
```bash
# Direct message input (focused after channel nav)
osascript -e 'tell application "System Events" to keystroke "@mybot hello"'
sleep 0.3
osascript -e 'tell application "System Events" to key code 36'
```
### Send Long Message
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
set the clipboard to "A long test message for the bot..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
### Slash Command Test
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
tell application "System Events"
keystroke "/ask What is the meaning of life?"
delay 0.5
key code 36
end tell
'
```
### Verify Response
```bash
sleep 10
screencapture /tmp/slack-bot-response.png
```
---
## Client: Telegram
**App name:** `Telegram` | **Process name:** `Telegram`
### Activate & Navigate
```bash
# Activate Telegram
osascript -e 'tell application "Telegram" to activate'
sleep 1
# Search for a bot (Cmd+F or click search)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "MyTestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
### Send Message to Bot
```bash
# After navigating to bot chat, input is focused
osascript -e '
tell application "System Events"
keystroke "/start"
delay 0.3
key code 36
end tell
'
```
### Send Long Message
```bash
osascript -e '
tell application "Telegram" to activate
delay 0.5
set the clipboard to "Tell me about quantum computing in detail"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
### Verify Response
```bash
sleep 10
screencapture /tmp/telegram-bot-response.png
```
### Telegram Bot API (programmatic alternative)
For sending messages directly to the bot's chat without UI:
```bash
# Send message as the bot (for testing webhooks/responses)
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID&text=test message"
# Get recent updates
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates?limit=5" | jq .
```
---
## Client: WeChat / 微信
**App name:** `微信` or `WeChat` | **Process name:** `WeChat`
### Activate & Navigate
```bash
# Activate WeChat
osascript -e 'tell application "微信" to activate'
sleep 1
# Search for a contact/bot (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "TestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
### Send Message
```bash
# After navigating to a chat, the input is focused
osascript -e '
tell application "System Events"
keystroke "Hello bot!"
delay 0.3
key code 36
end tell
'
```
### Send Long Message (clipboard)
```bash
osascript -e '
tell application "微信" to activate
delay 0.5
set the clipboard to "Please help me with this task..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
### Verify Response
```bash
sleep 10
screencapture /tmp/wechat-bot-response.png
```
### WeChat-Specific Notes
- WeChat macOS app name can be `微信` or `WeChat` depending on system language. Try both:
```bash
osascript -e 'tell application "微信" to activate' 2> /dev/null \
|| osascript -e 'tell application "WeChat" to activate'
```
- WeChat uses **Enter** to send (not Cmd+Enter by default, but configurable)
- For multi-line messages without sending, use **Shift+Enter**:
```bash
osascript -e 'tell application "System Events" to key code 36 using shift down'
```
---
## Client: Lark / 飞书
**App name:** `Lark` or `飞书` | **Process name:** `Lark` or `飞书`
### Activate & Navigate
```bash
# Activate Lark (auto-detects Lark or 飞书)
osascript -e 'tell application "Lark" to activate' 2> /dev/null \
|| osascript -e 'tell application "飞书" to activate'
sleep 1
# Quick Switcher / Search (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
### Send Message to Bot
```bash
osascript -e '
set the clipboard to "@MyBot help me with this task"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
### Verify Response
```bash
sleep 10
screencapture /tmp/lark-bot-response.png
```
### Lark-Specific Notes
- App name varies: `Lark` (international) vs `飞书` (China mainland) — the script auto-detects
- Uses `Cmd+K` for quick search (same as Discord/Slack)
- Enter sends message by default
---
## Client: QQ
**App name:** `QQ` | **Process name:** `QQ`
### Activate & Navigate
```bash
osascript -e 'tell application "QQ" to activate'
sleep 1
# Search for contact/group (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.8
end tell
'
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
### Send Message to Bot
```bash
osascript -e '
set the clipboard to "Hello bot!"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
### Verify Response
```bash
sleep 10
screencapture /tmp/qq-bot-response.png
```
### QQ-Specific Notes
- Enter sends message by default; Shift+Enter for newlines
- Uses `Cmd+F` for search
- Always use clipboard paste for CJK characters
---
## Common Bot Testing Workflow (osascript)
Regardless of platform, the pattern is:
```bash
APP_NAME="Discord" # or "Slack", "Telegram", "微信"
CHANNEL="bot-testing"
MESSAGE="Hello bot!"
WAIT_SECONDS=10
# 1. Activate
osascript -e "tell application \"$APP_NAME\" to activate"
sleep 1
# 2. Navigate to channel/chat (via Quick Switcher or Search)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e "tell application \"System Events\" to keystroke \"$CHANNEL\""
sleep 1
osascript -e 'tell application "System Events" to key code 36'
sleep 2
# 3. Send message
osascript -e "set the clipboard to \"$MESSAGE\""
osascript -e '
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 4. Wait for bot response
sleep "$WAIT_SECONDS"
# 5. Screenshot for verification
screencapture /tmp/"${APP_NAME,,}"-bot-test.png
echo "Result saved to /tmp/${APP_NAME,,}-bot-test.png"
```
### Tips
- **Use clipboard paste** (`Cmd+V`) for messages containing special characters or long text — `keystroke` can mangle non-ASCII
- **Add `delay`** between actions — apps need time to process UI events
- **Screenshot for verification** — use `screencapture` + `Read` tool for visual checks
- **Use a dedicated test channel/chat** — avoid polluting real conversations
- **Check app name** — some apps have different names in different locales (e.g., `微信` vs `WeChat`)
- **Accessibility permissions required** — System Events automation requires granting Accessibility access in System Preferences > Privacy & Security > Accessibility
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.
---
@@ -1006,6 +419,7 @@ Ready-to-use scripts in `.agents/skills/local-testing/scripts/`:
| `electron-dev.sh` | Manage Electron dev env (start/stop/status/restart) |
| `capture-app-window.sh` | Capture screenshot of a specific app window |
| `record-electron-demo.sh` | Record Electron app demo with ffmpeg |
| `record-app-screen.sh` | Record app screen (video + screenshots, start/stop) |
| `test-discord-bot.sh` | Send message to Discord bot via osascript |
| `test-slack-bot.sh` | Send message to Slack bot via osascript |
| `test-telegram-bot.sh` | Send message to Telegram bot via osascript |
@@ -1068,25 +482,16 @@ Each script: activates the app, navigates to the channel/contact, pastes the mes
# Screen Recording
Record automated demos by combining `ffmpeg` screen capture with `agent-browser` automation. The script `.agents/skills/local-testing/scripts/record-electron-demo.sh` handles the full lifecycle for Electron.
### Usage
Record automated demos using `record-app-screen.sh` (start/stop lifecycle, CDP screenshots + ffmpeg assembly). See [references/record-app-screen.md](references/record-app-screen.md) for full documentation.
```bash
# Run the built-in demo (queue-edit feature)
./.agents/skills/local-testing/scripts/record-electron-demo.sh
# Run a custom automation script
./.agents/skills/local-testing/scripts/record-electron-demo.sh ./my-demo.sh /tmp/my-demo.mp4
./.agents/skills/local-testing/scripts/electron-dev.sh start
./.agents/skills/local-testing/scripts/record-app-screen.sh start my-demo
# ... run automation ...
./.agents/skills/local-testing/scripts/record-app-screen.sh stop
```
The script automatically:
1. Starts Electron with CDP and waits for SPA to load
2. Detects window position, screen, and Retina scale via Swift/CGWindowList
3. Records only the Electron window region using `ffmpeg -f avfoundation` with crop
4. Runs the demo (built-in or custom script receiving CDP port as `$1`)
5. Stops recording and cleans up
Outputs to `.records/` directory (gitignored): `<name>.mp4` (video) + `<name>/` (screenshots every 3s).
---
@@ -1112,14 +517,4 @@ The script automatically:
### osascript
- **Accessibility permission required** — first run will prompt for access; grant it in System Preferences > Privacy & Security > Accessibility for Terminal / iTerm / Claude Code
- **`keystroke` is slow for long text** — always use clipboard paste (`Cmd+V`) for messages over \~20 characters
- **`keystroke` can mangle non-ASCII** — use clipboard paste for Chinese, emoji, or special characters
- **`key code 36` is Enter** — this is the hardware key code, works regardless of keyboard layout
- **`entire contents` is extremely slow** — avoid for complex UIs; use screenshots instead
- **App name varies by locale** — `微信` vs `WeChat`, `企业微信` vs `WeCom`; handle both
- **WeChat Enter sends immediately** — use `Shift+Enter` for newlines within a message
- **Rate limiting** — don't send messages too fast; platforms may throttle or flag automated input
- **Lark / 飞书 app name varies** — `Lark` (international) vs `飞书` (China mainland); scripts auto-detect
- **QQ uses `Cmd+F` for search** — not `Cmd+K` like Discord/Slack/Lark
- **Bot response times vary** — AI-powered bots may take 10-60s; use generous sleep values
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).
@@ -0,0 +1,97 @@
# Discord Bot Testing
**App name:** `Discord` | **Process name:** `Discord`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Discord
osascript -e 'tell application "Discord" to activate'
sleep 1
# Open Quick Switcher (Cmd+K) to navigate to a channel
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
## Send Message to Bot
```bash
# The message input is focused after navigating to a channel
# Type a message
osascript -e 'tell application "System Events" to keystroke "/hello"'
sleep 0.5
osascript -e 'tell application "System Events" to key code 36' # Enter
```
## Send Long Message (via clipboard)
```bash
osascript -e '
tell application "Discord" to activate
delay 0.5
set the clipboard to "Write a 3000 word essay about space exploration"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Bot Response
```bash
# Wait for bot to respond, then screenshot
sleep 10
screencapture /tmp/discord-bot-response.png
# Read with the Read tool for visual verification
```
## Full Bot Test Example
```bash
#!/usr/bin/env bash
# test-discord-bot.sh — Send message and verify bot response
# 1. Activate Discord and navigate to channel
osascript -e '
tell application "Discord" to activate
delay 1
-- Quick Switcher
tell application "System Events" to keystroke "k" using command down
delay 0.5
tell application "System Events" to keystroke "bot-testing"
delay 1
tell application "System Events" to key code 36
delay 2
'
# 2. Send test message
osascript -e '
set the clipboard to "!ping"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 3. Wait for response and capture
sleep 5
screencapture /tmp/discord-test-result.png
echo "Screenshot saved to /tmp/discord-test-result.png"
```
## Script
```bash
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "!ping"
./.agents/skills/local-testing/scripts/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
```
@@ -0,0 +1,61 @@
# Lark / 飞书 Bot Testing
**App name:** `Lark` or `飞书` | **Process name:** `Lark` or `飞书`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Lark (auto-detects Lark or 飞书)
osascript -e 'tell application "Lark" to activate' 2> /dev/null \
|| osascript -e 'tell application "飞书" to activate'
sleep 1
# Quick Switcher / Search (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
## Send Message to Bot
```bash
osascript -e '
set the clipboard to "@MyBot help me with this task"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/lark-bot-response.png
```
## Lark-Specific Notes
- App name varies: `Lark` (international) vs `飞书` (China mainland) — the script auto-detects
- Uses `Cmd+K` for quick search (same as Discord/Slack)
- Enter sends message by default
- Always use clipboard paste for CJK characters
## Script
```bash
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "@MyBot hello"
./.agents/skills/local-testing/scripts/test-lark-bot.sh "bot-testing" "Help me with this" 30
```
@@ -0,0 +1,217 @@
# osascript Common Patterns
Shared AppleScript / `osascript` patterns used by all platform bot tests. Read this first, then refer to the per-platform file for app-specific quirks.
## Core Patterns
### Activate an App
```bash
osascript -e 'tell application "Discord" to activate'
```
### Type Text
```bash
# Type character by character (reliable, but slow for long text)
osascript -e 'tell application "System Events" to keystroke "Hello world"'
# Press Enter
osascript -e 'tell application "System Events" to key code 36'
# Press Tab
osascript -e 'tell application "System Events" to key code 48'
# Press Escape
osascript -e 'tell application "System Events" to key code 53'
```
### Paste from Clipboard (fast, for long text)
```bash
# Set clipboard and paste — much faster than keystroke for long messages
osascript -e 'set the clipboard to "Your long message here"'
osascript -e 'tell application "System Events" to keystroke "v" using command down'
```
Or in one shot:
```bash
osascript -e '
set the clipboard to "Your long message here"
tell application "System Events" to keystroke "v" using command down
'
```
### Keyboard Shortcuts
```bash
# Cmd+K (quick switcher in Discord/Slack)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
# Cmd+F (search)
osascript -e 'tell application "System Events" to keystroke "f" using command down'
# Cmd+N (new message/chat)
osascript -e 'tell application "System Events" to keystroke "n" using command down'
# Cmd+Shift+K (example: multi-modifier)
osascript -e 'tell application "System Events" to keystroke "k" using {command down, shift down}'
```
### Click at Position
```bash
# Click at absolute screen coordinates
osascript -e '
tell application "System Events"
click at {500, 300}
end tell
'
```
### Get Window Info
```bash
# Get window position and size
osascript -e '
tell application "System Events"
tell process "Discord"
get {position, size} of window 1
end tell
end tell
'
```
### Screenshot
```bash
# Full screen
screencapture /tmp/screenshot.png
# Interactive region select
screencapture -i /tmp/screenshot.png
# Specific window (by window ID from CGWindowList)
screencapture -l < WINDOW_ID > /tmp/screenshot.png
```
To get window ID for a specific app:
```bash
osascript -e '
tell application "System Events"
tell process "Discord"
get id of window 1
end tell
end tell
'
```
### Read Accessibility Elements
```bash
# Get all UI elements of the frontmost window (can be slow/large)
osascript -e '
tell application "System Events"
tell process "Discord"
entire contents of window 1
end tell
end tell
'
# Get a specific element's value
osascript -e '
tell application "System Events"
tell process "Discord"
get value of text field 1 of window 1
end tell
end tell
'
```
> **Warning:** `entire contents` can be extremely slow on complex UIs. Prefer screenshots + `Read` tool for visual verification.
### Read Screen Text via Clipboard
For reading the latest message or response from an app:
```bash
# Select all text in the focused area and copy
osascript -e '
tell application "System Events"
keystroke "a" using command down
keystroke "c" using command down
end tell
'
sleep 0.5
# Read clipboard
pbpaste
```
---
## Common Bot Testing Workflow
Regardless of platform, the pattern is:
```bash
APP_NAME="Discord" # or "Slack", "Telegram", "微信"
CHANNEL="bot-testing"
MESSAGE="Hello bot!"
WAIT_SECONDS=10
# 1. Activate
osascript -e "tell application \"$APP_NAME\" to activate"
sleep 1
# 2. Navigate to channel/chat (via Quick Switcher or Search)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e "tell application \"System Events\" to keystroke \"$CHANNEL\""
sleep 1
osascript -e 'tell application "System Events" to key code 36'
sleep 2
# 3. Send message
osascript -e "set the clipboard to \"$MESSAGE\""
osascript -e '
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 4. Wait for bot response
sleep "$WAIT_SECONDS"
# 5. Screenshot for verification
screencapture /tmp/"${APP_NAME,,}"-bot-test.png
echo "Result saved to /tmp/${APP_NAME,,}-bot-test.png"
```
### Tips
- **Use clipboard paste** (`Cmd+V`) for messages containing special characters or long text — `keystroke` can mangle non-ASCII
- **Add `delay`** between actions — apps need time to process UI events
- **Screenshot for verification** — use `screencapture` + `Read` tool for visual checks
- **Use a dedicated test channel/chat** — avoid polluting real conversations
- **Check app name** — some apps have different names in different locales (e.g., `微信` vs `WeChat`)
- **Accessibility permissions required** — System Events automation requires granting Accessibility access in System Preferences > Privacy & Security > Accessibility
---
## Gotchas
- **Accessibility permission required** — first run will prompt for access; grant it in System Preferences > Privacy & Security > Accessibility for Terminal / iTerm / Claude Code
- **`keystroke` is slow for long text** — always use clipboard paste (`Cmd+V`) for messages over \~20 characters
- **`keystroke` can mangle non-ASCII** — use clipboard paste for Chinese, emoji, or special characters
- **`key code 36` is Enter** — this is the hardware key code, works regardless of keyboard layout
- **`entire contents` is extremely slow** — avoid for complex UIs; use screenshots instead
- **App name varies by locale** — `微信` vs `WeChat`, `企业微信` vs `WeCom`; handle both
- **WeChat Enter sends immediately** — use `Shift+Enter` for newlines within a message
- **Rate limiting** — don't send messages too fast; platforms may throttle or flag automated input
- **Lark / 飞书 app name varies** — `Lark` (international) vs `飞书` (China mainland); scripts auto-detect
- **QQ uses `Cmd+F` for search** — not `Cmd+K` like Discord/Slack/Lark
- **Bot response times vary** — AI-powered bots may take 10-60s; use generous sleep values
@@ -0,0 +1,62 @@
# QQ Bot Testing
**App name:** `QQ` | **Process name:** `QQ`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
osascript -e 'tell application "QQ" to activate'
sleep 1
# Search for contact/group (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.8
end tell
'
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
## Send Message to Bot
```bash
osascript -e '
set the clipboard to "Hello bot!"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/qq-bot-response.png
```
## QQ-Specific Notes
- Enter sends message by default; Shift+Enter for newlines
- Uses `Cmd+F` for search (not `Cmd+K` like Discord/Slack/Lark)
- Always use clipboard paste for CJK characters
## Script
```bash
./.agents/skills/local-testing/scripts/test-qq-bot.sh "bot-testing" "Hello bot" 15
./.agents/skills/local-testing/scripts/test-qq-bot.sh "MyBot" "/help" 10
```
@@ -0,0 +1,142 @@
# record-app-screen.sh
General-purpose screen recording tool for the Electron app. Captures CDP screenshots as video frames and gallery snapshots, then assembles into an MP4 on stop.
## Why CDP Screenshots Instead of ffmpeg Screen Capture
- **Works on any screen** — CDP screenshots capture the browser viewport directly, so external monitors, Retina scaling, and window positioning are all handled automatically
- **No signal handling issues** — ffmpeg-static (npm) produces corrupt MP4 files when killed (missing moov atom). CDP screenshots avoid this entirely
- **Consistent output** — Screenshots are resolution-independent and don't require crop coordinate calculations
## Commands
```bash
# Start recording (Electron must be running with CDP)
.agents/skills/local-testing/scripts/record-app-screen.sh start [output_name]
# Stop recording and assemble video
.agents/skills/local-testing/scripts/record-app-screen.sh stop
# Check if recording is active
.agents/skills/local-testing/scripts/record-app-screen.sh status
```
### Arguments
| Argument | Default | Description |
| ------------- | --------------------------- | -------------------------- |
| `output_name` | `recording-YYYYMMDD-HHMMSS` | Base name for output files |
### Environment Variables
| Variable | Default | Description |
| ---------------------- | ------- | -------------------------------------- |
| `CDP_PORT` | `9222` | Chrome DevTools Protocol port |
| `SCREENSHOT_INTERVAL` | `3` | Seconds between gallery screenshots |
| `VIDEO_FRAME_INTERVAL` | `0.5` | Seconds between video frames (\~2 fps) |
## Output Structure
```
.records/
<name>.mp4 # Video assembled from frames (~2 fps)
<name>/ # Gallery screenshots (every 3s)
0000.png
0001.png
0002.png
...
```
The `.records/` directory is at the project root and is gitignored.
## How It Works
### Start
1. Creates two background loops:
- **Video frames** — `agent-browser screenshot` every `VIDEO_FRAME_INTERVAL` seconds into a temp directory (`/tmp/record-frames-XXXXXX/`)
- **Gallery screenshots** — `agent-browser screenshot` every `SCREENSHOT_INTERVAL` seconds into `.records/<name>/`
2. Saves PIDs and paths to `/tmp/record-app-screen.pids` and `/tmp/record-app-screen.state`
### Stop
1. Kills both background loops
2. Assembles video frames into MP4 using ffmpeg:
```
ffmpeg -framerate 2 -i frame_%06d.png -c:v libx264 -crf 23 -pix_fmt yuv420p <output>.mp4
```
3. Cleans up temp frame directory
4. Reports file sizes and paths
## Usage Examples
### Basic Test Recording
```bash
# Start Electron
.agents/skills/local-testing/scripts/electron-dev.sh start
# Start recording
.agents/skills/local-testing/scripts/record-app-screen.sh start my-test
# Run automation
agent-browser --cdp 9222 click @e61
agent-browser --cdp 9222 type @e42 "hello"
agent-browser --cdp 9222 press Enter
sleep 10
# Stop and get results
.agents/skills/local-testing/scripts/record-app-screen.sh stop
# → .records/my-test.mp4 + .records/my-test/*.png
```
### Gateway Streaming Demo
```bash
.agents/skills/local-testing/scripts/electron-dev.sh start
# Inject gateway URL
agent-browser --cdp 9222 eval --stdin << 'EOF'
(function() {
var store = window.global_serverConfigStore;
store.setState({ serverConfig: { ...store.getState().serverConfig,
agentGatewayUrl: 'https://agent-gateway.lobehub.com' } });
return 'ready';
})()
EOF
# Record
.agents/skills/local-testing/scripts/record-app-screen.sh start gateway-demo
# Navigate to agent, send message, wait for completion...
# (automation commands here)
.agents/skills/local-testing/scripts/record-app-screen.sh stop
open .records/gateway-demo.mp4
```
### Check Active Recording
```bash
.agents/skills/local-testing/scripts/record-app-screen.sh status
# [record] Active recording
# Frames: 42 captured (running: yes)
# Screenshots: 14 captured (running: yes)
# Output: .records/my-test.mp4
```
## Prerequisites
- **ffmpeg** — For video assembly. Install via `bun add -g ffmpeg-static` or `brew install ffmpeg`
- **agent-browser** — For CDP screenshots. Install via `npm i -g agent-browser`
- **Electron app running** — With CDP enabled (use `electron-dev.sh start`)
## Troubleshooting
| Problem | Solution |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| "No active recording found" on stop | PID file was cleaned up. Check if background processes are still running with `ps aux \| grep agent-browser` |
| "A recording is already active" | Run `stop` first, or manually clean: `rm /tmp/record-app-screen.pids /tmp/record-app-screen.state` |
| Video is 0 bytes | No frames were captured. Ensure Electron is running and CDP port is correct |
| Screenshots are blank/white | SPA may not have loaded yet. Wait for `electron-dev.sh` to report "Renderer ready" |
| ffmpeg assembly fails | Check `/tmp/ffmpeg-assemble.log`. Ensure ffmpeg is installed and frames exist |
@@ -0,0 +1,73 @@
# Slack Bot Testing
**App name:** `Slack` | **Process name:** `Slack`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Slack
osascript -e 'tell application "Slack" to activate'
sleep 1
# Quick Switcher (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
## Send Message to Bot
```bash
# Direct message input (focused after channel nav)
osascript -e 'tell application "System Events" to keystroke "@mybot hello"'
sleep 0.3
osascript -e 'tell application "System Events" to key code 36'
```
## Send Long Message
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
set the clipboard to "A long test message for the bot..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Slash Command Test
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
tell application "System Events"
keystroke "/ask What is the meaning of life?"
delay 0.5
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/slack-bot-response.png
```
## Script
```bash
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "@mybot hello"
./.agents/skills/local-testing/scripts/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
```
@@ -0,0 +1,80 @@
# Telegram Bot Testing
**App name:** `Telegram` | **Process name:** `Telegram`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Telegram
osascript -e 'tell application "Telegram" to activate'
sleep 1
# Search for a bot (Cmd+F or click search)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "MyTestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
## Send Message to Bot
```bash
# After navigating to bot chat, input is focused
osascript -e '
tell application "System Events"
keystroke "/start"
delay 0.3
key code 36
end tell
'
```
## Send Long Message
```bash
osascript -e '
tell application "Telegram" to activate
delay 0.5
set the clipboard to "Tell me about quantum computing in detail"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/telegram-bot-response.png
```
## Telegram Bot API (programmatic alternative)
For sending messages directly to the bot's chat without UI:
```bash
# Send message as the bot (for testing webhooks/responses)
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID&text=test message"
# Get recent updates
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates?limit=5" | jq .
```
## Script
```bash
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "MyTestBot" "/start"
./.agents/skills/local-testing/scripts/test-telegram-bot.sh "GPTBot" "Hello" 60
```
@@ -0,0 +1,81 @@
# WeChat / 微信 Bot Testing
**App name:** `微信` or `WeChat` | **Process name:** `WeChat`
See [osascript-common.md](./osascript-common.md) for shared patterns.
## Activate & Navigate
```bash
# Activate WeChat
osascript -e 'tell application "微信" to activate'
sleep 1
# Search for a contact/bot (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "TestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
## Send Message
```bash
# After navigating to a chat, the input is focused
osascript -e '
tell application "System Events"
keystroke "Hello bot!"
delay 0.3
key code 36
end tell
'
```
## Send Long Message (clipboard)
```bash
osascript -e '
tell application "微信" to activate
delay 0.5
set the clipboard to "Please help me with this task..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/wechat-bot-response.png
```
## WeChat-Specific Notes
- WeChat macOS app name can be `微信` or `WeChat` depending on system language. Try both:
```bash
osascript -e 'tell application "微信" to activate' 2> /dev/null \
|| osascript -e 'tell application "WeChat" to activate'
```
- WeChat uses **Enter** to send (not Cmd+Enter by default, but configurable)
- For multi-line messages without sending, use **Shift+Enter**:
```bash
osascript -e 'tell application "System Events" to key code 36 using shift down'
```
- Always use clipboard paste for CJK characters — `keystroke` mangles non-ASCII
## Script
```bash
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "文件传输助手" "test message" 5
./.agents/skills/local-testing/scripts/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
```
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
#
# record-app-screen.sh — Record the Electron app window (video + screenshots)
#
# Captures screenshots via agent-browser (CDP), then assembles into video on stop.
# Works on any screen (including external monitors) since it uses CDP, not screen capture.
#
# Usage:
# ./record-app-screen.sh start [output_name] # Begin recording
# ./record-app-screen.sh stop # Stop and save
# ./record-app-screen.sh status # Check recording state
#
# Outputs to .records/ directory:
# .records/<name>.mp4 — Video assembled from screenshots (~2 fps)
# .records/<name>/ — Screenshots every SCREENSHOT_INTERVAL seconds
#
# Prerequisites:
# - ffmpeg installed (bun add -g ffmpeg-static, or brew install ffmpeg)
# - agent-browser CLI installed
# - Electron app already running with CDP enabled
#
# Environment variables:
# CDP_PORT — Chrome DevTools Protocol port (default: 9222)
# SCREENSHOT_INTERVAL — Seconds between gallery screenshots (default: 3)
# VIDEO_FRAME_INTERVAL — Seconds between video frames (default: 0.5)
#
# Examples:
# ./electron-dev.sh start
# ./record-app-screen.sh start gateway-demo
# # ... run automation via agent-browser ...
# ./record-app-screen.sh stop
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
RECORDS_DIR="$PROJECT_DIR/.records"
PID_FILE="/tmp/record-app-screen.pids"
STATE_FILE="/tmp/record-app-screen.state"
CDP_PORT="${CDP_PORT:-9222}"
SCREENSHOT_INTERVAL="${SCREENSHOT_INTERVAL:-3}"
VIDEO_FRAME_INTERVAL="${VIDEO_FRAME_INTERVAL:-0.5}"
AB="agent-browser --cdp $CDP_PORT"
# ─── Commands ───
cmd_start() {
local output_name="${1:-recording-$(date +%Y%m%d-%H%M%S)}"
local output_video="$RECORDS_DIR/${output_name}.mp4"
local screenshot_dir="$RECORDS_DIR/${output_name}"
local frames_dir
frames_dir=$(mktemp -d /tmp/record-frames-XXXXXX)
if [ -f "$PID_FILE" ]; then
echo "[record] A recording is already active. Run '$0 stop' first."
exit 1
fi
mkdir -p "$RECORDS_DIR" "$screenshot_dir"
# Video frames loop (~2 fps via agent-browser CDP screenshots)
(
local idx=0
while true; do
local fname
fname=$(printf "%s/frame_%06d.png" "$frames_dir" "$idx")
$AB screenshot "$fname" 2>/dev/null || true
idx=$((idx + 1))
sleep "$VIDEO_FRAME_INTERVAL"
done
) &
local frames_pid=$!
# Gallery screenshots loop (every N seconds for human review)
(
local idx=0
while true; do
local fname
fname=$(printf "%s/%04d.png" "$screenshot_dir" "$idx")
$AB screenshot "$fname" 2>/dev/null || true
idx=$((idx + 1))
sleep "$SCREENSHOT_INTERVAL"
done
) &
local screenshot_pid=$!
# Save state
echo "$frames_pid $screenshot_pid" > "$PID_FILE"
echo "$output_video $frames_dir $screenshot_dir" > "$STATE_FILE"
echo "[record] Started!"
echo " Video frames: every ${VIDEO_FRAME_INTERVAL}s (PID $frames_pid)"
echo " Screenshots: every ${SCREENSHOT_INTERVAL}s → $screenshot_dir/"
echo " Stop with: $0 stop"
}
cmd_stop() {
if [ ! -f "$PID_FILE" ] || [ ! -f "$STATE_FILE" ]; then
echo "[record] No active recording found."
return 0
fi
local frames_pid screenshot_pid
read -r frames_pid screenshot_pid < "$PID_FILE"
local output_video frames_dir screenshot_dir
read -r output_video frames_dir screenshot_dir < "$STATE_FILE"
# Stop both capture loops
kill "$frames_pid" 2>/dev/null || true
kill "$screenshot_pid" 2>/dev/null || true
wait "$frames_pid" 2>/dev/null || true
wait "$screenshot_pid" 2>/dev/null || true
# Assemble frames into video
local frame_count
frame_count=$(ls -1 "$frames_dir"/frame_*.png 2>/dev/null | wc -l | tr -d ' ')
if [ "$frame_count" -gt 0 ]; then
echo "[record] Assembling $frame_count frames into video..."
ffmpeg -y -framerate 2 -i "$frames_dir/frame_%06d.png" \
-c:v libx264 -crf 23 -pix_fmt yuv420p -an \
"$output_video" > /tmp/ffmpeg-assemble.log 2>&1
if [ ! -s "$output_video" ]; then
echo " [warn] Video assembly failed. Check /tmp/ffmpeg-assemble.log"
echo " Frames preserved in: $frames_dir/"
fi
else
echo " [warn] No frames captured."
fi
rm -rf "$frames_dir" 2>/dev/null
rm -f "$PID_FILE" "$STATE_FILE"
local video_size screenshot_count
video_size=$(ls -lh "$output_video" 2>/dev/null | awk '{print $5}' || echo "?")
screenshot_count=$(ls -1 "$screenshot_dir"/*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
echo "[record] Stopped!"
echo " Video: $output_video ($video_size)"
echo " Screenshots: ${screenshot_count} files in $screenshot_dir/"
echo " Play: open $output_video"
}
cmd_status() {
if [ ! -f "$PID_FILE" ]; then
echo "[record] No active recording."
return 0
fi
local frames_pid screenshot_pid
read -r frames_pid screenshot_pid < "$PID_FILE"
local frames_ok="no" screenshot_ok="no"
kill -0 "$frames_pid" 2>/dev/null && frames_ok="yes"
kill -0 "$screenshot_pid" 2>/dev/null && screenshot_ok="yes"
if [ -f "$STATE_FILE" ]; then
local output_video frames_dir screenshot_dir
read -r output_video frames_dir screenshot_dir < "$STATE_FILE"
local frame_count ss_count
frame_count=$(ls -1 "$frames_dir"/frame_*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
ss_count=$(ls -1 "$screenshot_dir"/*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
echo "[record] Active recording"
echo " Frames: $frame_count captured (running: $frames_ok)"
echo " Screenshots: $ss_count captured (running: $screenshot_ok)"
echo " Output: $output_video"
fi
}
# ─── Main ───
case "${1:-}" in
start) shift; cmd_start "$@" ;;
stop) cmd_stop ;;
status) cmd_status ;;
*)
echo "Usage: $0 {start [name] | stop | status}"
echo ""
echo " start [name] Start recording (default: recording-YYYYMMDD-HHMMSS)"
echo " stop Stop recording and save outputs"
echo " status Check if recording is active"
exit 1
;;
esac
+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`
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: pr
description: "Create a PR for the current branch. Use when the user asks to create a pull request, submit PR, or says 'pr'."
user_invocable: true
user-invocable: true
---
# Create Pull Request
+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
+6 -3
View File
@@ -71,15 +71,18 @@ internal_createTopic: async (params) => {
**Actions:**
- Public: `createTopic`, `sendMessage`
- Internal: `internal_createTopic`, `internal_updateMessageContent`
- Dispatch: `internal_dispatchTopic`
- Toggle: `internal_toggleMessageLoading`
**State:**
**State:**
- ID arrays: `topicEditingIds`
- ID arrays: `messageLoadingIds`, `topicEditingIds`
- Maps: `topicMaps`, `messagesMap`
- Active: `activeTopicId`
- Init flags: `topicsInit`
## Detailed Guides
@@ -30,16 +30,13 @@ internal_createMessage: async (message, context) => {
let tempId = context?.tempMessageId;
if (!tempId) {
tempId = internal_createTmpMessage(message);
internal_toggleMessageLoading(true, tempId);
}
try {
const id = await messageService.createMessage(message);
await refreshMessages();
internal_toggleMessageLoading(false, tempId);
return id;
} catch (e) {
internal_toggleMessageLoading(false, tempId);
internal_dispatchMessage({
id: tempId,
type: 'updateMessage',
+16
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
@@ -408,3 +413,14 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# IMPORTANT: This key is stored server-side only and NEVER exposed to the client
# When this key is set, Klavis integration will be automatically enabled
# KLAVIS_API_KEY=your_klavis_api_key_here
# #######################################
# #### Message Gateway (IM Integration) ##
# #######################################
# External message-gateway for unified IM platform connection management.
# Set ENABLED=1 to activate. To migrate away, remove ENABLED first (keep URL/TOKEN)
# so LobeHub can automatically disconnect leftover gateway connections.
# MESSAGE_GATEWAY_ENABLED=1
# MESSAGE_GATEWAY_URL=https://message-gateway.lobehub.com
# MESSAGE_GATEWAY_SERVICE_TOKEN=your_service_token_here
+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() }}
+9 -1
View File
@@ -25,6 +25,9 @@ Desktop.ini
*.code-workspace
.vscode/sessions.json
prd
# Recordings
.records/
# Temporary files
.temp/
temp/
@@ -137,5 +140,10 @@ pnpm-lock.yaml
.turbo
spaHtmlTemplates.ts
# Embedded CLI bundle (built at pack time)
apps/desktop/resources/bin/lobe-cli.js
apps/desktop/resources/cli-package.json
# Superpowers plugin brainstorm/spec outputs (local only; do not commit)
.superpowers/
docs/superpowers
.heerogeneous-tracing
+4 -4
View File
@@ -1,6 +1,6 @@
const { defineConfig } = require('@lobehub/i18n-cli');
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
module.exports = defineConfig({
entry: 'locales/en-US',
@@ -27,14 +27,14 @@ module.exports = defineConfig({
],
temperature: 0,
saveImmediately: true,
modelName: 'chatgpt-4o-latest',
modelName: 'gpt-5.1-chat-latest',
experimental: {
jsonMode: true,
},
markdown: {
reference:
'You need to maintain the component format of the mdx file; the output text does not need to be wrapped in any code block syntax on the outermost layer.\n' +
fs.readFileSync(path.join(__dirname, 'docs/glossary.md'), 'utf-8'),
fs.readFileSync(path.join(__dirname, 'docs/glossary.md'), 'utf8'),
entry: ['./README.md', './docs/**/*.md', './docs/**/*.mdx'],
entryLocale: 'en-US',
outputLocales: ['zh-CN'],
+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
+4 -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.3" "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
@@ -98,6 +98,9 @@ Manage messages
.B model
Manage AI models
.TP
.B notify
Send a callback message to a topic and trigger the agent to process it
.TP
.B provider
Manage AI providers
.TP
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/cli",
"version": "0.0.3",
"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'
+27 -1
View File
@@ -37,7 +37,25 @@ export async function getAuthInfo(): Promise<AuthInfo> {
};
}
export async function getAgentStreamAuthInfo(): Promise<Pick<AuthInfo, 'headers' | 'serverUrl'>> {
export type AgentStreamTokenType = 'jwt' | 'apiKey';
export interface AgentStreamAuthInfo {
headers: Record<string, string>;
serverUrl: string;
/**
* Raw token value (without header prefix). Used for WebSocket auth messages
* where header-based auth is not available.
*/
token: string;
/**
* How the token should be verified by downstream services (agent gateway WS).
* jwt → validate with JWKS
* apiKey → validate by calling /api/v1/users/me
*/
tokenType: AgentStreamTokenType;
}
export async function getAgentStreamAuthInfo(): Promise<AgentStreamAuthInfo> {
const serverUrl = resolveServerUrl();
const envJwt = process.env.LOBEHUB_JWT;
@@ -45,6 +63,8 @@ export async function getAgentStreamAuthInfo(): Promise<Pick<AuthInfo, 'headers'
return {
headers: { 'Oidc-Auth': envJwt },
serverUrl,
token: envJwt,
tokenType: 'jwt',
};
}
@@ -53,6 +73,8 @@ export async function getAgentStreamAuthInfo(): Promise<Pick<AuthInfo, 'headers'
return {
headers: { 'X-API-Key': envApiKey },
serverUrl,
token: envApiKey,
tokenType: 'apiKey',
};
}
@@ -64,11 +86,15 @@ export async function getAgentStreamAuthInfo(): Promise<Pick<AuthInfo, 'headers'
return {
headers: {},
serverUrl,
token: '',
tokenType: 'jwt',
};
}
return {
headers: { 'Oidc-Auth': result.credentials.accessToken },
serverUrl,
token: result.credentials.accessToken,
tokenType: 'jwt',
};
}
+13 -2
View File
@@ -258,6 +258,10 @@ export function registerAgentCommand(program: Command) {
'--device <target>',
'Target device ID, or use "local" for the current connected device',
)
.option(
'--no-headless',
"Disable headless mode and wait for human approval on tool calls (default: headless — tools auto-run, matching the CLI's non-interactive nature)",
)
.option('--json', 'Output full JSON event stream')
.option('-v, --verbose', 'Show detailed tool call info')
.option('--replay <file>', 'Replay events from a saved JSON file (offline)')
@@ -267,6 +271,7 @@ export function registerAgentCommand(program: Command) {
agentId?: string;
autoStart?: boolean;
device?: string;
headless?: boolean;
json?: boolean;
prompt?: string;
replay?: string;
@@ -340,6 +345,11 @@ export function registerAgentCommand(program: Command) {
if (options.slug) input.slug = options.slug;
if (options.topicId) input.appContext = { topicId: options.topicId };
if (options.autoStart === false) input.autoStart = false;
// commander's --no-headless sets `headless` to false. Anything else
// (undefined, true) → headless mode is on and tool calls auto-execute.
if (options.headless !== false) {
input.userInterventionConfig = { approvalMode: 'headless' };
}
const result = await client.aiAgent.execAgent.mutate(input as any);
const r = result as any;
@@ -355,16 +365,17 @@ export function registerAgentCommand(program: Command) {
}
// 2. Connect to stream (WebSocket via Gateway, or fallback to SSE)
const { serverUrl, headers } = await getAgentStreamAuthInfo();
const { serverUrl, headers, token, tokenType } = await getAgentStreamAuthInfo();
const agentGatewayUrl = options.sse ? undefined : resolveAgentGatewayUrl();
if (agentGatewayUrl) {
const token = headers['Oidc-Auth'] || headers['X-API-Key'] || '';
await streamAgentEventsViaWebSocket({
gatewayUrl: agentGatewayUrl,
json: options.json,
operationId,
serverUrl,
token,
tokenType,
verbose: options.verbose,
});
} else {
+42
View File
@@ -270,6 +270,48 @@ describe('generate command', () => {
);
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Video generation started'));
});
it('should pass image-to-video params', async () => {
mockTrpcClient.generationTopic.createTopic.mutate.mockResolvedValue('topic-3');
mockTrpcClient.video.createVideo.mutate.mockResolvedValue({
data: { generationId: 'gen-v2' },
success: true,
});
const program = createProgram();
await program.parseAsync([
'node',
'test',
'generate',
'video',
'a cat waving',
'--model',
'cogvideox',
'--provider',
'zhipu',
'--image',
'https://example.com/first.png',
'--end-image',
'https://example.com/last.png',
'--images',
'https://example.com/a.png',
'https://example.com/b.png',
]);
expect(mockTrpcClient.video.createVideo.mutate).toHaveBeenCalledWith(
expect.objectContaining({
generationTopicId: 'topic-3',
model: 'cogvideox',
params: {
endImageUrl: 'https://example.com/last.png',
imageUrl: 'https://example.com/first.png',
imageUrls: ['https://example.com/a.png', 'https://example.com/b.png'],
prompt: 'a cat waving',
},
provider: 'zhipu',
}),
);
});
});
describe('tts', () => {
+10 -1
View File
@@ -6,13 +6,16 @@ import { getTrpcClient } from '../../api/client';
export function registerVideoCommand(parent: Command) {
parent
.command('video <prompt>')
.description('Generate a video from text')
.description('Generate a video from text or image(s)')
.requiredOption('-m, --model <model>', 'Model ID')
.requiredOption('-p, --provider <provider>', 'Provider name')
.option('--aspect-ratio <ratio>', 'Aspect ratio (e.g. 16:9)')
.option('--duration <sec>', 'Duration in seconds')
.option('--resolution <res>', 'Resolution (e.g. 720p, 1080p)')
.option('--seed <n>', 'Random seed')
.option('--image <url>', 'First-frame image URL (image-to-video)')
.option('--images <urls...>', 'Multiple reference image URLs')
.option('--end-image <url>', 'Last-frame image URL')
.option('--json', 'Output raw JSON')
.action(
async (
@@ -20,6 +23,9 @@ export function registerVideoCommand(parent: Command) {
options: {
aspectRatio?: string;
duration?: string;
endImage?: string;
image?: string;
images?: string[];
json?: boolean;
model: string;
provider: string;
@@ -35,6 +41,9 @@ export function registerVideoCommand(parent: Command) {
if (options.duration) params.duration = Number.parseInt(options.duration, 10);
if (options.resolution) params.resolution = options.resolution;
if (options.seed) params.seed = Number.parseInt(options.seed, 10);
if (options.image) params.imageUrl = options.image;
if (options.images && options.images.length > 0) params.imageUrls = options.images;
if (options.endImage) params.endImageUrl = options.endImage;
const result = await client.video.createVideo.mutate({
generationTopicId: topicId as string,
+51
View File
@@ -79,6 +79,57 @@ describe('message command', () => {
);
expect(mockTrpcClient.message.listAll.query).not.toHaveBeenCalled();
});
it('should keep first page on the backend default offset for filtered queries', async () => {
mockTrpcClient.message.getMessages.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync([
'node',
'test',
'message',
'list',
'--topic-id',
't1',
'-L',
'200',
]);
expect(mockTrpcClient.message.getMessages.query).toHaveBeenCalledWith(
expect.objectContaining({ pageSize: 200, topicId: 't1' }),
);
});
it('should convert page 2 to current 1 for filtered queries', async () => {
mockTrpcClient.message.getMessages.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync([
'node',
'test',
'message',
'list',
'--topic-id',
't1',
'--page',
'2',
]);
expect(mockTrpcClient.message.getMessages.query).toHaveBeenCalledWith(
expect.objectContaining({ current: 1, topicId: 't1' }),
);
});
it('should support the short page flag for filtered queries', async () => {
mockTrpcClient.message.getMessages.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync(['node', 'test', 'message', 'list', '--topic-id', 't1', '-P', '2']);
expect(mockTrpcClient.message.getMessages.query).toHaveBeenCalledWith(
expect.objectContaining({ current: 1, topicId: 't1' }),
);
});
});
describe('search', () => {
+4 -2
View File
@@ -16,7 +16,7 @@ export function registerMessageCommand(program: Command) {
.option('--topic-id <id>', 'Filter by topic ID')
.option('--agent-id <id>', 'Filter by agent ID')
.option('-L, --limit <n>', 'Page size', '30')
.option('--page <n>', 'Page number', '1')
.option('-P, --page <n>', 'Page number', '1')
.option('--user', 'Only show user messages')
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
.action(
@@ -32,7 +32,9 @@ export function registerMessageCommand(program: Command) {
const hasFilter = options.topicId || options.agentId;
const pageSize = options.limit ? Number.parseInt(options.limit, 10) : undefined;
const current = options.page ? Number.parseInt(options.page, 10) : undefined;
const current = options.page
? Math.max(Number.parseInt(options.page, 10) - 1, 0)
: undefined;
let items: any[];
+1 -1
View File
@@ -208,7 +208,7 @@ function readAgentProfile(workspacePath: string): AgentProfile {
// Try to extract **Emoji:** value (single emoji)
const emojiMatch = content.match(/\*{0,2}Emoji:?\*{0,2}\s*(.+)/i);
const rawAvatar = emojiMatch ? emojiMatch[1].trim() : undefined;
// Filter out placeholder text like (待定), _(待定)_, (TBD), N/A, etc.
// Filter out placeholder text like (待定)(Chinese TBD), _(待定)_, (TBD), N/A, etc.
const isPlaceholder =
rawAvatar && /^[_*(].*[)_*]$|^(?:tbd|todo|n\/?a|none|待定|未定)$/i.test(rawAvatar);
const avatar = rawAvatar && !isPlaceholder ? rawAvatar : undefined;
+51
View File
@@ -0,0 +1,51 @@
import type { Command } from 'commander';
import pc from 'picocolors';
import { getTrpcClient } from '../api/client';
import { log } from '../utils/logger';
export function registerNotifyCommand(program: Command) {
program
.command('notify')
.description('Send a callback message to a topic and trigger the agent to process it')
.requiredOption('--topic <topicId>', 'Target topic ID')
.requiredOption('-c, --content <content>', 'Message content')
.option('--agent-id <agentId>', 'Agent ID (overrides topic default)')
.option('--thread-id <threadId>', 'Thread ID for threaded conversations')
.option('--json', 'Output JSON')
.action(
async (options: {
agentId?: string;
content: string;
json?: boolean;
threadId?: string;
topic: string;
}) => {
log.debug('notify: topic=%s, agentId=%s', options.topic, options.agentId);
const client = await getTrpcClient();
try {
const result = await client.agentNotify.notify.mutate({
agentId: options.agentId,
content: options.content,
threadId: options.threadId,
topicId: options.topic,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
console.log(`${pc.green('✓')} Message sent to topic ${pc.bold(result.topicId)}`);
if (result.operationId) {
console.log(` Operation ID: ${result.operationId}`);
}
} catch (error: any) {
console.error(`${pc.red('✗')} Failed to send notification: ${error.message}`);
process.exit(1);
}
},
);
}
+7 -2
View File
@@ -56,7 +56,7 @@ export function registerTaskCommand(program: Command) {
const client = await getTrpcClient();
const input: Record<string, any> = {};
if (options.status) input.status = options.status;
if (options.status) input.statuses = [options.status];
if (options.root) input.parentTaskId = null;
if (options.parent) input.parentTaskId = options.parent;
if (options.agent) input.assigneeAgentId = options.agent;
@@ -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}`,
+42
View File
@@ -77,6 +77,48 @@ describe('topic command', () => {
expect.objectContaining({ agentId: 'a1' }),
);
});
it('should keep first page on the backend default offset', async () => {
mockTrpcClient.topic.getTopics.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync(['node', 'test', 'topic', 'list', '--agent-id', 'a1', '-L', '200']);
expect(mockTrpcClient.topic.getTopics.query).toHaveBeenCalledWith(
expect.objectContaining({ agentId: 'a1', pageSize: 200 }),
);
});
it('should convert page 2 to current 1', async () => {
mockTrpcClient.topic.getTopics.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync([
'node',
'test',
'topic',
'list',
'--agent-id',
'a1',
'--page',
'2',
]);
expect(mockTrpcClient.topic.getTopics.query).toHaveBeenCalledWith(
expect.objectContaining({ agentId: 'a1', current: 1 }),
);
});
it('should support the short page flag', async () => {
mockTrpcClient.topic.getTopics.query.mockResolvedValue([]);
const program = createProgram();
await program.parseAsync(['node', 'test', 'topic', 'list', '--agent-id', 'a1', '-P', '2']);
expect(mockTrpcClient.topic.getTopics.query).toHaveBeenCalledWith(
expect.objectContaining({ agentId: 'a1', current: 1 }),
);
});
});
describe('search', () => {
+3 -2
View File
@@ -17,7 +17,7 @@ export function registerTopicCommand(program: Command) {
.description('List topics')
.option('--agent-id <id>', 'Filter by agent ID')
.option('-L, --limit <n>', 'Page size', '30')
.option('--page <n>', 'Page number', '1')
.option('-P, --page <n>', 'Page number', '1')
.option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)')
.action(
async (options: {
@@ -31,7 +31,8 @@ export function registerTopicCommand(program: Command) {
const input: Record<string, any> = {};
if (options.agentId) input.agentId = options.agentId;
if (options.limit) input.pageSize = Number.parseInt(options.limit, 10);
if (options.page) input.current = Number.parseInt(options.page, 10);
const page = options.page ? Number.parseInt(options.page, 10) : undefined;
if (page !== undefined && page > 1) input.current = page - 1;
const result = await client.topic.getTopics.query(input as any);
const items = Array.isArray(result) ? result : ((result as any).items ?? []);
+1 -1
View File
@@ -160,7 +160,7 @@ export function spawnDaemon(args: string[]): number {
// Re-run the same entry with --daemon-child (internal flag)
const child = spawn(process.execPath, [...process.execArgv, ...args, '--daemon-child'], {
detached: true,
env: { ...process.env, LOBEHUB_DAEMON: '1' },
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', LOBEHUB_DAEMON: '1' },
stdio: ['ignore', logFd, logFd],
});
+1 -1
View File
@@ -1,3 +1,3 @@
import { createProgram } from './program';
createProgram().parse();
createProgram().parse(process.argv, { from: 'node' });
+2
View File
@@ -22,6 +22,7 @@ import { registerMemoryCommand } from './commands/memory';
import { registerMessageCommand } from './commands/message';
import { registerMigrateCommand } from './commands/migrate';
import { registerModelCommand } from './commands/model';
import { registerNotifyCommand } from './commands/notify';
import { registerPluginCommand } from './commands/plugin';
import { registerProviderCommand } from './commands/provider';
import { registerSearchCommand } from './commands/search';
@@ -68,6 +69,7 @@ export function createProgram() {
registerTopicCommand(program);
registerMessageCommand(program);
registerModelCommand(program);
registerNotifyCommand(program);
registerProviderCommand(program);
registerPluginCommand(program);
registerUserCommand(program);
+28 -1
View File
@@ -279,8 +279,10 @@ describe('streamAgentEventsViaWebSocket', () => {
await flush();
const ws = capturedWs!;
// Note: serverUrl is not set here, and JSON.stringify drops undefined keys,
// so the parsed auth message will not contain a `serverUrl` field.
expect(ws.sent.map((s) => JSON.parse(s))).toEqual([
{ token: 'test-token', type: 'auth' },
{ token: 'test-token', tokenType: 'jwt', type: 'auth' },
{ lastEventId: '', type: 'resume' },
]);
@@ -288,6 +290,31 @@ describe('streamAgentEventsViaWebSocket', () => {
await promise;
});
it('should send tokenType=apiKey and serverUrl when the caller uses an API key', async () => {
const promise = streamAgentEventsViaWebSocket({
gatewayUrl: 'https://gw.test.com',
operationId: 'op-1',
serverUrl: 'https://app.lobehub.com',
token: 'lh_sk_abc',
tokenType: 'apiKey',
});
await flush();
const ws = capturedWs!;
// serverUrl is forwarded so the gateway can call back to /api/v1/users/me
// to verify the API key.
expect(ws.sent.map((s) => JSON.parse(s))[0]).toEqual({
serverUrl: 'https://app.lobehub.com',
token: 'lh_sk_abc',
tokenType: 'apiKey',
type: 'auth',
});
ws.simulateMessage({ id: '1', type: 'session_complete' });
await promise;
});
it('should render agent_event messages using existing renderEvent', async () => {
const promise = streamAgentEventsViaWebSocket({
gatewayUrl: 'https://gw.test.com',
+19 -11
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;
@@ -20,7 +14,18 @@ interface StreamOptions {
interface WebSocketStreamOptions extends StreamOptions {
gatewayUrl: string;
operationId: string;
/**
* LobeHub server URL the gateway should call back to when verifying
* an apiKey token (via `/api/v1/users/me`). Required when
* `tokenType === 'apiKey'`; ignored for JWT.
*/
serverUrl?: string;
token: string;
/**
* How the gateway should verify `token`. `jwt` is the default for
* backwards compatibility with existing callers.
*/
tokenType?: 'jwt' | 'apiKey';
}
/**
@@ -168,13 +173,13 @@ const HEARTBEAT_INTERVAL = 30_000;
export async function streamAgentEventsViaWebSocket(
options: WebSocketStreamOptions,
): Promise<void> {
const { gatewayUrl, operationId, token, ...streamOpts } = options;
const { gatewayUrl, operationId, serverUrl, token, tokenType = 'jwt', ...streamOpts } = options;
const wsUrl = urlJoin(
gatewayUrl.replace(/^http/, 'ws'),
`/ws?operationId=${encodeURIComponent(operationId)}`,
);
log.debug(`Connecting to gateway: ${wsUrl}`);
log.debug(`Connecting to gateway: ${wsUrl} (auth: ${tokenType})`);
return new Promise<void>((resolve, reject) => {
const ws = new WebSocket(wsUrl);
@@ -192,7 +197,10 @@ export async function streamAgentEventsViaWebSocket(
};
ws.onopen = () => {
ws.send(JSON.stringify({ token, type: 'auth' }));
// `serverUrl` is required so the gateway can call back to verify an
// apiKey token. Harmless (but unused) for JWT, so we always include it
// when available to match the device-gateway-client contract.
ws.send(JSON.stringify({ serverUrl, token, tokenType, type: 'auth' }));
};
ws.onmessage = (event) => {
+4
View File
@@ -9,6 +9,10 @@ export default defineConfig({
entry: ['src/index.ts'],
fixedExtension: false,
format: ['esm'],
minify: !!process.env.MINIFY,
outputOptions: {
codeSplitting: false,
},
platform: 'node',
target: 'node18',
});
+4
View File
@@ -1,3 +1,7 @@
## 专题文档
- [桌面端全屏 Overlay 截图方案设计与集成说明](./WindowOverlayCapture.md)
## 核心框架组件目录架构
### 主进程核心组件
+502
View File
@@ -0,0 +1,502 @@
# 桌面端全屏 Overlay 截图方案设计与集成说明
| 字段 | 内容 |
| ------------ | ----------------------------------------------------- |
| 状态 | 已完成技术预研与 demo 验证 |
| 最后更新 | 2026-04-14 |
| 适用范围 | Electron 桌面端全屏遮罩、窗口高亮、点击截窗、区域截图 |
| 当前验证载体 | `tmp/electron-window-overlay-demo` |
| 目标读者 | 后续将该能力接入 LobeHub Desktop 主业务的开发者 |
## 1. 文档目标
本文档用于沉淀以下内容:
| 目标 | 说明 |
| -------------------- | ------------------------------------------------------------- |
| 记录方案演进 | 保存从纯 Electron、native、自研、开源库到最终 demo 的决策过程 |
| 固化关键技术结论 | 明确哪些能力 Electron 可做,哪些能力必须借助额外库 |
| 提供业务接入蓝图 | 指出应修改的真实仓库文件、模块边界、IPC 设计与 UI 接入点 |
| 降低后续重复调研成本 | 使后续实现可以直接沿用本文档,不必重新验证底层假设 |
## 2. 需求回顾
| 需求项 | 结论 |
| ----------------------------------- | --------------------------------------------------- |
| 新增一个 “全屏” 入口 | 需要,但本质上是一个覆盖整块屏幕的透明 overlay 窗口 |
| 覆盖用户整个 screen | 需要,且在 macOS 上要覆盖菜单栏与 Dock 所在区域 |
| 获取系统窗口几何信息 | 需要,至少需要 `appName + bounds + windowId` |
| 在 overlay 上高亮窗口边框并显示 Tag | 需要 |
| 点击高亮窗口即截图该窗口 | 需要 |
| 拖拽任意区域截图 | 需要 |
| 输出先写入剪贴板 | 需要,作为 MVP |
| 避免自研 native addon | 明确要求避免 |
| 跨平台预留 | 需要,至少不能被 macOS-only 自研方案锁死 |
## 3. 关键术语澄清
### 3.1 “压住 macOS 菜单栏与 Dock” 的准确含义
这里的含义不是 “调用系统 fullscreen API”,而是:
| 项目 | 含义 |
| -------- | ------------------------------------------------------------ |
| 覆盖范围 | 窗口尺寸必须基于 `display.bounds`,而不是 `display.workArea` |
| Z 轴层级 | 窗口需要位于普通应用窗口之上,并且进入菜单栏所在区域 |
| 视觉效果 | 用户看到的是整块屏幕都被半透明遮罩覆盖 |
必须区分以下两件事:
| 易混概念 | 实际含义 |
| ----------------------------------- | ---------------------------------------------------- |
| `app.dock.hide()` | 仅隐藏应用在 Dock 中的图标,不会隐藏系统 Dock 栏本身 |
| `BrowserWindow.setFullScreen(true)` | 更接近原生全屏行为,未必适合作为截图 overlay |
## 4. 预研结论总览
### 4.1 方案对比
| 方案 | 能否覆盖菜单栏 / Dock | 能否拿到系统窗口 bounds | 能否按窗口截图 | 跨平台性 | 结论 |
| ------------------------------------- | --------------------: | ----------------------: | -----------------: | -------: | -------------------------- |
| 纯 Electron `desktopCapturer` | 是 | 否 | 部分可做,但不精确 | 高 | 不足以满足需求 |
| 自研 native addon | 是 | 是 | 是 | 中 | 能做,但被明确拒绝 |
| 参考 Claude.app 的 native quick entry | 是 | 是 | 是 | 低到中 | 可借鉴思路,不适合直接照搬 |
| `node-screenshots` 单库 | 是 | 是 | 是 | 中到高 | 核心方案成立 |
| `node-screenshots + get-windows` | 是 | 是 | 是 | 中到高 | 当前最终方案 |
### 4.2 最终选型
| 能力 | 最终实现 |
| --------------------- | -------------------------- |
| 全屏 overlay 窗口 | Electron `BrowserWindow` |
| 系统窗口枚举 | `node-screenshots` |
| 指定窗口截图 | `node-screenshots` |
| 隐藏 / 伪关闭窗口过滤 | `get-windows` 作为白名单 |
| 区域截图 | Electron `desktopCapturer` |
| 输出介质 | `clipboard.writeImage()` |
## 5. 对 Claude.app 的观察结论
本轮曾直接检查过本机解包后的 Claude.app 产物,结论如下:
| 观察对象 | 结论 |
| ------------------ | ------------------------------------------------------------------------------------------------ |
| `quick_window` | 不是全屏 overlay;它是小尺寸 `panel` 弹窗 |
| `nativeQuickEntry` | Claude.app 存在原生 quick entry 能力,说明其真实覆盖式入口并不完全依赖纯 Electron |
| `cu-glow` | 这是最接近本需求的 Electron overlay 实现:使用 `display.bounds`、透明窗、`screen-saver` 置顶层级 |
据此可以得出两个重要判断:
| 判断 | 含义 |
| -------------------------------------------- | ---- |
| Electron 可以做 “整屏遮罩” | 成立 |
| Claude 的 “整屏入口” 并不等于 `quick_window` | 成立 |
## 6. 当前 demo 的最终方案
### 6.1 架构图
```text
┌──────────────────────────────┐
│ Tray / Menu / Future Action │
└──────────────┬───────────────┘
│ startOverlaySession
┌────────────────────────────────────────────┐
│ Main Process │
│ │
│ 1. 选定当前光标所在 display │
│ 2. 枚举窗口:node-screenshots │
│ 3. 过滤隐藏窗口:get-windows 白名单 │
│ 4. 创建整屏 overlay BrowserWindow │
└──────────────┬─────────────────────────────┘
│ preload / IPC
┌────────────────────────────────────────────┐
│ Overlay Renderer │
│ │
│ 1. 渲染窗口高亮框与左上角 tag │
│ 2. 点击窗口 => captureWindow(windowId) │
│ 3. 拖拽区域 => captureRect(rect) │
└──────────────┬─────────────────────────────┘
│ IPC
┌────────────────────────────────────────────┐
│ Main Process Capture Path │
│ │
│ Window: node-screenshots.captureImage() │
│ Region: desktopCapturer + crop │
│ Output: clipboard.writeImage() │
└────────────────────────────────────────────┘
```
### 6.2 demo 文件职责
| 文件 | 作用 |
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| [`tmp/electron-window-overlay-demo/main.mjs`](../../tmp/electron-window-overlay-demo/main.mjs) | 主进程入口;创建 overlay,枚举窗口,执行截图 |
| [`tmp/electron-window-overlay-demo/preload.cjs`](../../tmp/electron-window-overlay-demo/preload.cjs) | 为 overlay renderer 暴露 IPC bridge |
| [`tmp/electron-window-overlay-demo/renderer/index.html`](../../tmp/electron-window-overlay-demo/renderer/index.html) | overlay 渲染宿主页 |
| [`tmp/electron-window-overlay-demo/renderer/app.js`](../../tmp/electron-window-overlay-demo/renderer/app.js) | 窗口高亮、点击截窗、拖拽截区交互 |
| [`tmp/electron-window-overlay-demo/renderer/styles.css`](../../tmp/electron-window-overlay-demo/renderer/styles.css) | 遮罩视觉样式 |
| [`tmp/electron-window-overlay-demo/README.md`](../../tmp/electron-window-overlay-demo/README.md) | demo 的运行说明 |
## 7. 全屏 overlay 的关键实现参数
### 7.1 必要窗口参数
| 参数 / 调用 | 用途 | 必要性 |
| ----------------------------------- | ---------------------------------- | ------ |
| `x/y/width/height = display.bounds` | 覆盖整块屏幕,包括菜单栏区域 | 必需 |
| `transparent: true` | 允许渲染半透明遮罩 | 必需 |
| `frame: false` | 去除系统边框 | 必需 |
| `skipTaskbar: true` | 避免出现在任务栏 / Dock 窗口列表中 | 建议 |
| `hasShadow: false` | 避免覆盖层产生自身投影 | 建议 |
| `focusable: true` | 允许接收鼠标交互 | 必需 |
| `fullscreenable: false` | 避免进入原生 fullscreen 流程 | 建议 |
| `enableLargerThanScreen: true` | 提升跨平台稳健性 | 建议 |
| `type: 'panel'`macOS) | 更接近工具层窗口行为 | 建议 |
### 7.2 必要层级调用
| 调用 | 作用 |
| ---------------------------------------------------------------- | --------------------------------- |
| `setAlwaysOnTop(true, 'screen-saver')` | 让窗口位于更高层级 |
| `setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })` | 避免 Space / 全屏窗口场景下不可见 |
| `setHiddenInMissionControl(true)` | 降低该窗口对系统窗口管理的干扰 |
### 7.3 重要结论
| 结论 | 说明 |
| ------------------------- | ------------------------------------------- |
| `display.workArea` 不可用 | 它会排除菜单栏 / Dock 区域 |
| `display.bounds` 必须使用 | 只有它能覆盖整个 display |
| `screen-saver` 层级有效 | 这是当前 macOS 上最接近需求的 Electron 方案 |
## 8. 系统窗口枚举与过滤策略
### 8.1 为什么不能只用 Electron
| Electron 能力 | 缺口 |
| --------------------------------------------------- | --------------------------------------------------------- |
| `desktopCapturer.getSources({ types: ['window'] })` | 能列出可捕获源,但没有稳定的窗口 bounds 用于 overlay 画框 |
| `DesktopCapturerSource.thumbnail` | 可截图缩略图,但不适合 “按原窗口精确高亮 + 点击即截” |
因此,纯 Electron 不足以完成 “系统窗口高亮 + 点击截窗”。
### 8.2 `node-screenshots` 的职责
| API | 用途 |
| --------------------------------- | -------------- |
| `Window.all()` | 枚举系统窗口 |
| `window.id()` | 稳定识别窗口 |
| `window.appName()` | 获取应用名 |
| `window.title()` | 获取标题 |
| `window.x()/y()/width()/height()` | 获取几何信息 |
| `window.captureImage()` | 截取该窗口图像 |
### 8.3 `get-windows` 的职责
`get-windows` 在当前方案中不负责截图,而只负责 “第二层白名单过滤”。
| 问题 | 处理方式 |
| ------------------------------------------ | ------------------------------------------------------------- |
| 某些应用逻辑上已隐藏,但底层枚举仍可能残留 | 只保留同时出现在 `get-windows``node-screenshots` 中的窗口 |
| Electron 自身的假关闭 /hide 行为 | 该白名单对这类情况更稳 |
### 8.4 当前过滤规则
| 规则 | 目的 |
| ------------------------------------------------ | ---------------------------- |
| `isMinimized() === false` | 排除最小化窗口 |
| 最小尺寸阈值:`80x60` | 排除菜单栏控件、过小悬浮面板 |
| 排除 `Dock` / `Window Server` / `Control Centre` | 排除系统 UI |
| 排除 demo 自身窗口 | 避免 overlay 自我高亮 |
| 必须与目标 display 相交 | 只画当前屏幕可见窗口 |
| 必须出现在 `get-windows` 白名单中 | 排除隐藏 / 伪关闭残留窗口 |
## 9. 截图路径设计
### 9.1 点击窗口截图
```text
点击高亮框
└───> renderer 发送 windowId
└───> main 查找对应 node-screenshots Window
└───> overlay.hide()
└───> captureImage()
└───> PNG Buffer
└───> nativeImage
└───> clipboard.writeImage()
```
### 9.2 拖拽区域截图
```text
拖拽区域
└───> renderer 发送全局 rect
└───> main 隐藏 overlay
└───> desktopCapturer 获取目标 display 图像
└───> 按 scaleFactor 计算 cropRect
└───> clipboard.writeImage()
```
### 9.3 为什么两条路径采用不同技术
| 路径 | 技术 | 原因 |
| ---------- | ------------------ | --------------------------------- |
| 按窗口截图 | `node-screenshots` | 它天然理解 “窗口” 这一对象 |
| 按区域截图 | `desktopCapturer` | 区域本质上是 display 上的矩形裁剪 |
## 10. 权限与平台边界
### 10.1 macOS 权限
| 权限 | 是否需要 | 用途 |
| ---------------- | ---------------- | ----------------------------------------------------- |
| Screen Recording | 需要 | 窗口截图、区域截图 |
| Accessibility | 当前方案不强依赖 | `get-windows` 已使用 `accessibilityPermission: false` |
### 10.2 当前已知平台边界
| 平台 / 场景 | 状态 | 说明 |
| ------------- | -------- | --------------------------------------------------------------------- |
| macOS | 已验证 | 当前主要验证平台 |
| Windows | 理论可行 | `node-screenshots` / `get-windows` 均支持,但尚未在本仓库内做实机验证 |
| Linux X11 | 理论可行 | 需要单独验证打包与权限 |
| Linux Wayland | 风险较高 | 上游库虽宣称支持,但必须做专项验证 |
### 10.3 特殊窗口风险
| 风险类型 | 当前处理 |
| ---------------------- | -------------------------------------------------------------- |
| 菜单栏状态窗 / 面板 | 通过尺寸阈值与排除名单降低噪音 |
| 系统 UI | 通过应用名黑名单排除 |
| 某些应用截图结果为黑图 | 已观察到个别状态面板存在此现象,应在业务层继续限制候选窗口类别 |
## 11. 已完成验证
| 验证项 | 结果 | 产物 |
| ----------------------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| overlay 覆盖整屏 | 通过 | [`tmp/electron-window-overlay-demo/.cache/window-overlay-visual.png`](../../tmp/electron-window-overlay-demo/.cache/window-overlay-visual.png) |
| `node-screenshots` 直接截图普通窗口 | 通过 | [`tmp/electron-window-overlay-demo/.cache/cursor-direct.png`](../../tmp/electron-window-overlay-demo/.cache/cursor-direct.png) |
| 点击高亮窗口后写入剪贴板 | 通过 | [`tmp/electron-window-overlay-demo/.cache/window-capture-probe-final.png`](../../tmp/electron-window-overlay-demo/.cache/window-capture-probe-final.png) |
| 拖拽区域截图 | 通过 | [`tmp/electron-window-overlay-demo/.cache/self-test-clipboard-final.png`](../../tmp/electron-window-overlay-demo/.cache/self-test-clipboard-final.png) |
## 12. 推荐的业务接入方式
### 12.1 总体建议
| 维度 | 建议 |
| -------------------- | ---------------------------------------------------------------------------------- |
| overlay 窗口生命周期 | 不建议直接挂进现有 `BrowserManager` 的常规窗口体系 |
| 原因 | overlay 是瞬态、全屏、平台特化、不可持久化的工具窗口,与主业务窗口生命周期明显不同 |
| 推荐做法 | 新增独立主进程模块管理 overlay;渲染内容仍建议走现有 SPA 路由体系 |
### 12.2 为什么不直接复用 `BrowserManager`
| 观察 | 影响 |
| ----------------------------------------- | ------------------------------- |
| `Browser` 默认承担普通业务窗口职责 | overlay 并非普通业务窗口 |
| `WindowStateManager` 倾向保存窗口状态 | overlay 不应持久化位置与大小 |
| `BrowserManager` 以 “可复用业务窗口” 建模 | overlay 更接近 “一次性工具会话” |
因此,更合理的做法是:
```text
┌────────────────────────────┐
│ BrowserManager │ 负责常规业务窗口
└────────────────────────────┘
┌────────────────────────────┐
│ CaptureOverlayManager │ 负责全屏截图 overlay 会话
└────────────────────────────┘
```
## 13. 建议的生产代码落点
### 13.1 主进程
| 建议文件 | 作用 |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `apps/desktop/src/main/modules/screenCapture/CaptureOverlayManager.ts` | 创建 / 销毁 overlay 窗口;管理一次截图会话 |
| `apps/desktop/src/main/modules/screenCapture/WindowSourceService.ts` | 封装 `node-screenshots + get-windows` 的窗口枚举与过滤 |
| `apps/desktop/src/main/modules/screenCapture/CaptureService.ts` | 封装窗口截图、区域截图、剪贴板输出 |
| `apps/desktop/src/main/modules/screenCapture/permission.ts` | 封装 macOS 屏幕录制权限检查 |
| `apps/desktop/src/main/controllers/ScreenCaptureCtr.ts` | 对 renderer 暴露 `start / captureRect / captureWindow / close` IPC |
| `apps/desktop/src/main/controllers/registry.ts` | 注册 `ScreenCaptureCtr` |
### 13.2 IPC 类型
| 建议文件 | 作用 |
| --------------------------------------------------------- | ------------------------------------------------- |
| `packages/electron-client-ipc/src/types/screenCapture.ts` | 定义 overlay 会话、窗口元数据、截图参数与返回结果 |
| `packages/electron-client-ipc/src/types/index.ts` | 导出新类型 |
建议定义的核心类型:
| 类型名 | 用途 |
| -------------------------- | --------------------------------------------------- |
| `ScreenCaptureDisplayInfo` | display id / bounds / scaleFactor |
| `ScreenCaptureWindowInfo` | `windowId/appName/title/bounds/overlayBounds/order` |
| `ScreenCaptureSession` | `display + windows` |
| `CaptureRectParams` | 全局屏幕坐标的矩形 |
| `ScreenCaptureStartResult` | 权限状态、会话状态、错误信息 |
| `ScreenCaptureOutput` | `clipboard`、后续可扩展 `file``attachment` |
### 13.3 Preload 与 renderer service
| 建议文件 | 作用 |
| ----------------------------------------- | -------------------------------------------------- |
| `apps/desktop/src/preload/electronApi.ts` | 通常无需特殊改造;沿用统一 `invoke` 即可 |
| `src/services/electron/screenCapture.ts` | 前端统一调用 `ensureElectronIpc().screenCapture.*` |
### 13.4 Renderer 路由
生产环境存在两种可选实现:
| 方案 | 优点 | 缺点 | 建议 |
| ------------------ | -------------------------------- | -------------------------------- | ---------------- |
| 独立静态 HTML 页面 | 轻量、与业务隔离、最接近 demo | 与现有 React/i18n / 业务状态脱节 | 仅适合 spike |
| 独立桌面 SPA 路由 | 可复用现有构建、i18n、业务事件链 | 需要维护 desktop router 双配置 | **推荐生产使用** |
若采用 SPA 路由,建议新增:
| 建议文件 | 作用 |
| ------------------------------------------------------- | ------------------------------------ |
| `src/routes/(desktop)/screen-capture-overlay/index.tsx` | overlay 页面入口;仅负责挂载 UI 组件 |
| `src/features/DesktopScreenCaptureOverlay/*` | 业务组件、hooks、样式 |
| `src/spa/router/desktopRouter.config.tsx` | 动态路由配置 |
| `src/spa/router/desktopRouter.config.desktop.tsx` | 同步路由配置 |
必须注意:
| 规则 | 说明 |
| -------------------------------- | ------------------------------------ |
| 两份 desktop router 必须同时更新 | 否则 Electron 本地构建可能出现空白页 |
| overlay route 应保持极薄 | 不在 route 文件中堆叠业务逻辑 |
## 14. 托盘入口的真实接入点
若要从托盘启动 overlay,会涉及以下文件:
| 文件 | 作用 |
| ----------------------------------------------- | -------------------- |
| `apps/desktop/src/main/menus/impls/macOS.ts` | macOS 托盘菜单模板 |
| `apps/desktop/src/main/menus/impls/windows.ts` | Windows 托盘菜单模板 |
| `apps/desktop/src/main/menus/impls/linux.ts` | Linux 托盘菜单模板 |
| `apps/desktop/src/main/locales/default/menu.ts` | 托盘菜单文案 |
推荐新增文案键:
| Key | 语义 |
| -------------------------- | ------------------------ |
| `tray.captureScreen` | 启动截图 overlay |
| `tray.captureScreenWindow` | 启动窗口截图模式(可选) |
## 15. 业务接入分阶段计划
### 阶段一:桌面主进程能力落地
| 步骤 | 目标 |
| ---- | ---------------------------------------------------------------------------------- |
| 1 | 将 `node-screenshots``get-windows` 加入 `apps/desktop/package.json#dependencies` |
| 2 | 新建 `screenCapture` 主进程模块与 controller |
| 3 | 跑通托盘菜单触发 overlay |
| 4 | 继续以剪贴板为唯一输出 |
### 阶段二:接回现有业务 UI
| 步骤 | 目标 |
| ---- | -------------------------------------------------- |
| 1 | 新增桌面专用 overlay route /feature |
| 2 | 将截图结果从 “仅写剪贴板” 升级为 “回传 attachment” |
| 3 | 支持从 chat 输入区触发 |
| 4 | 支持截图后自动插入当前会话 |
### 阶段三:体验完善
| 步骤 | 目标 |
| ---- | ------------------------------------ |
| 1 | 多 display 支持 |
| 2 | Hover 高亮 / 文案优化 |
| 3 | 保存文件、编辑器标注、OCR 等增强能力 |
| 4 | 平台差异补齐(尤其 Windows / Linux |
## 16. 依赖落点与版本建议
### 16.1 应加入的位置
| 文件 | 说明 |
| --------------------------- | --------------------------------- |
| `apps/desktop/package.json` | Electron 桌面运行时的真实依赖落点 |
### 16.2 建议依赖
| 包名 | 用途 | 当前 demo 使用版本 |
| ------------------ | --------------------------- | ------------------ |
| `node-screenshots` | 枚举窗口 + 窗口截图 | `^0.2.8` |
| `get-windows` | 白名单过滤隐藏 / 伪关闭窗口 | `^9.3.0` |
说明:
| 项目 | 结论 |
| ---------------------------- | ---- |
| 这不是 “纯 Electron” 方案 | 成立 |
| 这也不是 “自研 native addon” | 成立 |
| 当前依赖的是开源原生库 | 成立 |
## 17. 测试建议
建议避免写 “窗口列表快照” 这类低信号测试,优先做行为测试。
| 测试层级 | 建议内容 |
| -------------- | ---------------------------------------------------------- |
| 单元测试 | 过滤逻辑:尺寸阈值、系统应用排除、自身窗口排除、白名单交集 |
| 主进程集成测试 | 权限失败、overlay 会话生命周期、错误分支 |
| 手工验证 | 菜单栏覆盖、点击截窗、拖拽截区、隐藏窗口过滤 |
建议手工验证清单:
| 检查项 | 期望 |
| ------------------------ | ------------------------ |
| 当前活动屏幕启动 overlay | 只覆盖当前目标 display |
| 已隐藏的 Electron 子窗口 | 不再出现边框 |
| 点击普通应用窗口 | 剪贴板中得到该窗口图像 |
| 拖拽区域截图 | 剪贴板中得到对应裁剪区域 |
| 取消操作 | `Esc` 可关闭 overlay |
## 18. 当前已确认的非目标
| 非目标 | 说明 |
| ----------------------------------- | ----------------------------------------------------------------------- |
| 当前阶段支持全平台一致体验 | 尚未完成 |
| 当前阶段支持窗口标题绝对准确 | `get-windows` 在无额外权限时标题可为空;当前主要依赖 `node-screenshots` |
| 当前阶段支持多 display 同时 overlay | 尚未实现 |
| 当前阶段支持标注编辑器 | 未实现 |
## 19. 后续实现时的推荐决策
| 决策点 | 推荐 |
| ----------------------------------------------- | ------------------------ |
| overlay 窗口是否复用 `BrowserManager` | 不推荐 |
| renderer 是否走 SPA route | 推荐 |
| 主进程是否继续保留 “剪贴板优先” 输出 | 推荐,先保持最小可用闭环 |
| 是否继续保留 `desktopCapturer` 作为区域截图路径 | 推荐 |
| 是否用 `get-windows` 继续做白名单过滤 | 推荐 |
## 20. 实施摘要
```text
┌──────────────────────────────────────────────┐
│ 已验证的技术事实 │
├──────────────────────────────────────────────┤
│ 1. Electron 可以创建覆盖整块 display 的窗体 │
│ 2. 纯 Electron 无法独立完成系统窗口高亮 │
│ 3. node-screenshots 可完成窗口枚举与截窗 │
│ 4. get-windows 可帮助过滤隐藏 / 残留窗口 │
│ 5. 最终可形成“点击窗口即截图 + 拖拽截区”闭环 │
└──────────────────────────────────────────────┘
```
本文档可视为后续将该能力正式接入 `apps/desktop` 主业务的实施基线。
+25 -1
View File
@@ -109,6 +109,26 @@ const config = {
console.info('📦 Downloading agent-browser binary...');
execSync('node scripts/download-agent-browser.mjs', { stdio: 'inherit', cwd: __dirname });
// Build and copy CLI bundle for embedding
console.info('📦 Building CLI for embedding...');
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);
// Write a minimal package.json next to the CLI bundle so that
// createRequire('../package.json') resolves correctly in the packaged app.
// The CLI script lives at Resources/bin/lobe-cli.js, so '../package.json'
// resolves to Resources/package.json.
const cliPkg = JSON.parse(
await fs.readFile(path.resolve(__dirname, '../cli/package.json'), 'utf8'),
);
await fs.writeFile(
path.resolve(__dirname, 'resources/cli-package.json'),
JSON.stringify({ name: cliPkg.name, type: 'module', version: cliPkg.version }),
);
console.info('✅ CLI bundle copied to resources/bin/lobe-cli.js');
},
/**
* AfterPack hook for post-processing:
@@ -235,6 +255,7 @@ const config = {
generateUpdatesFilesForAllChannels: true,
linux: {
category: 'Utility',
icon: 'build/icon.png',
maintainer: 'electronjs.org',
target: ['AppImage', 'snap', 'deb', 'rpm', 'tar.gz'],
},
@@ -296,7 +317,10 @@ const config = {
releaseNotes: process.env.RELEASE_NOTES || undefined,
},
extraResources: [{ from: 'resources/bin', to: 'bin' }],
extraResources: [
{ from: 'resources/bin', to: 'bin' },
{ from: 'resources/cli-package.json', to: 'package.json' },
],
win: {
executableName: 'LobeHub',
+78 -8
View File
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import dotenv from 'dotenv';
import { defineConfig } from 'electron-vite';
import type { PluginOption, ViteDevServer } from 'vite';
@@ -15,15 +16,69 @@ 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 === '/overlay' || pathname === '/overlay.html') {
req.url = '/apps/desktop/overlay.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();
});
@@ -43,6 +98,8 @@ const updateChannel = process.env.UPDATE_CHANNEL;
const desktopPackageJson = JSON.parse(
readFileSync(path.resolve(__dirname, 'package.json'), 'utf8'),
) as { version: string };
const electronRuntimeExternals = ['electron'];
const mainProcessRuntimeExternals = [...electronRuntimeExternals, 'node-mac-permissions'];
console.info(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`);
@@ -51,10 +108,15 @@ export default defineConfig({
build: {
minify: !isDev,
outDir: 'dist/main',
rollupOptions: {
rolldownOptions: {
// Native modules must be externalized to work correctly.
// bufferutil and utf-8-validate are optional peer deps of ws that may not be installed.
external: [...getExternalDependencies(), 'bufferutil', 'utf-8-validate'],
external: [
...mainProcessRuntimeExternals,
...getExternalDependencies(),
'bufferutil',
'utf-8-validate',
],
output: {
// Prevent debug package from being bundled into index.js to avoid side-effect pollution
manualChunks(id) {
@@ -88,9 +150,11 @@ export default defineConfig({
build: {
minify: !isDev,
outDir: 'dist/preload',
rolldownOptions: {
external: electronRuntimeExternals,
},
sourcemap: isDev ? 'inline' : false,
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/main'),
@@ -102,8 +166,12 @@ export default defineConfig({
root: ROOT_DIR,
build: {
outDir: path.resolve(__dirname, 'dist/renderer'),
rollupOptions: {
input: path.resolve(__dirname, 'index.html'),
rolldownOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
overlay: path.resolve(__dirname, 'overlay.html'),
popup: path.resolve(__dirname, 'popup.html'),
},
output: sharedRollupOutput,
},
},
@@ -113,7 +181,9 @@ export default defineConfig({
},
optimizeDeps: sharedOptimizeDeps,
plugins: [
forceAbsoluteBasePlugin(),
electronDesktopHtmlPlugin(),
vanillaExtractPlugin(),
...(sharedRendererPlugins({ platform: 'desktop' }) as PluginOption[]),
],
resolve: {
+2 -1
View File
@@ -36,7 +36,8 @@ export const nativeModules = [
// macOS-only native modules
...(isDarwin ? ['node-mac-permissions'] : []),
'@napi-rs/canvas',
// Add more native modules here as needed
'get-windows',
'node-screenshots',
];
/**
+15
View File
@@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body, #root { width: 100%; height: 100%; overflow: hidden; background: transparent; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/apps/desktop/src/overlay/entry.tsx"></script>
</body>
</html>
+10 -3
View File
@@ -11,6 +11,7 @@
"author": "LobeHub",
"main": "./dist/main/index.js",
"scripts": {
"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",
@@ -41,7 +42,10 @@
"update-server": "sh scripts/update-test/run-test.sh"
},
"dependencies": {
"@napi-rs/canvas": "^0.1.70"
"@lobehub/fluent-emoji": "^4.1.0",
"@napi-rs/canvas": "^0.1.70",
"get-windows": "^9.3.0",
"node-screenshots": "^0.2.8"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
@@ -63,6 +67,8 @@
"@types/semver": "^7.7.1",
"@types/set-cookie-parser": "^2.4.10",
"@typescript/native-preview": "7.0.0-dev.20251210.1",
"@vanilla-extract/css": "^1.17.4",
"@vanilla-extract/vite-plugin": "^5.1.0",
"async-retry": "^1.3.3",
"consola": "^3.4.2",
"cookie": "^1.1.1",
@@ -75,7 +81,7 @@
"electron-log": "^5.4.3",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"electron-vite": "^5.0.0",
"electron-vite": "6.0.0-beta.1",
"electron-window-state": "^5.0.3",
"es-toolkit": "^1.43.0",
"eslint": "10.0.0",
@@ -95,13 +101,14 @@
"resolve": "^1.22.11",
"semver": "^7.7.3",
"set-cookie-parser": "^2.7.2",
"strip-ansi": "6.0.1",
"stylelint": "^15.11.0",
"superjson": "^2.2.6",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"undici": "^7.16.0",
"uuid": "^13.0.0",
"vite": "^7.3.1",
"vite": "^8.0.9",
"vitest": "^3.2.4",
"zod": "^3.25.76"
},
+5
View File
@@ -1,8 +1,13 @@
packages:
- '../cli'
- '../../packages/agent-gateway-client'
- '../../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>
+2 -1
View File
@@ -86,5 +86,6 @@
"window.minimize": "تصغير",
"window.title": "نافذة",
"window.toggleFullscreen": "تبديل وضع ملء الشاشة",
"window.zoom": "تكبير"
"window.zoom": "تكبير",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Минимизирай",
"window.title": "Прозорец",
"window.toggleFullscreen": "Превключи на цял екран",
"window.zoom": "Мащаб"
"window.zoom": "Мащаб",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Minimieren",
"window.title": "Fenster",
"window.toggleFullscreen": "Vollbild umschalten",
"window.zoom": "Zoom"
"window.zoom": "Zoom",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -71,6 +71,8 @@
"macOS.services": "Services",
"macOS.unhide": "Show All",
"tray.open": "Open {{appName}}",
"tray.openMiniToolbar": "Quick Composer",
"tray.quickChat": "Quick Chat",
"tray.quit": "Quit",
"tray.show": "Show {{appName}}",
"view.forceReload": "Force Reload",
@@ -86,5 +86,6 @@
"window.minimize": "Minimizar",
"window.title": "Ventana",
"window.toggleFullscreen": "Alternar pantalla completa",
"window.zoom": "Zoom"
"window.zoom": "Zoom",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "کوچک کردن",
"window.title": "پنجره",
"window.toggleFullscreen": "تغییر به حالت تمام صفحه",
"window.zoom": "زوم"
"window.zoom": "زوم",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Réduire",
"window.title": "Fenêtre",
"window.toggleFullscreen": "Basculer en plein écran",
"window.zoom": "Zoom"
"window.zoom": "Zoom",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Minimizza",
"window.title": "Finestra",
"window.toggleFullscreen": "Attiva/disattiva schermo intero",
"window.zoom": "Zoom"
"window.zoom": "Zoom",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "最小化",
"window.title": "ウィンドウ",
"window.toggleFullscreen": "フルスクリーン切替",
"window.zoom": "ズーム"
"window.zoom": "ズーム",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "최소화",
"window.title": "창",
"window.toggleFullscreen": "전체 화면 전환",
"window.zoom": "줌"
"window.zoom": "줌",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Minimaliseren",
"window.title": "Venster",
"window.toggleFullscreen": "Schakel volledig scherm in/uit",
"window.zoom": "Inzoomen"
"window.zoom": "Inzoomen",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Zminimalizuj",
"window.title": "Okno",
"window.toggleFullscreen": "Przełącz tryb pełnoekranowy",
"window.zoom": "Powiększenie"
"window.zoom": "Powiększenie",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Minimizar",
"window.title": "Janela",
"window.toggleFullscreen": "Alternar Tela Cheia",
"window.zoom": "Zoom"
"window.zoom": "Zoom",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Свернуть",
"window.title": "Окно",
"window.toggleFullscreen": "Переключить полноэкранный режим",
"window.zoom": "Масштаб"
"window.zoom": "Масштаб",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Küçült",
"window.title": "Pencere",
"window.toggleFullscreen": "Tam Ekrana Geç",
"window.zoom": "Yakınlaştır"
"window.zoom": "Yakınlaştır",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -86,5 +86,6 @@
"window.minimize": "Thu nhỏ",
"window.title": "Cửa sổ",
"window.toggleFullscreen": "Chuyển đổi toàn màn hình",
"window.zoom": "Thu phóng"
"window.zoom": "Thu phóng",
"tray.openMiniToolbar": "Quick Composer"
}
@@ -48,6 +48,7 @@
"file.newAgent": "新建助手",
"file.newAgentGroup": "新建助手组",
"file.newPage": "新建页面",
"file.newTab": "新建标签页",
"file.newTopic": "新建话题",
"file.preferences": "设置…",
"file.quit": "退出",
@@ -71,6 +72,8 @@
"macOS.services": "服务",
"macOS.unhide": "全部显示",
"tray.open": "打开 {{appName}}",
"tray.openMiniToolbar": "Quick Composer",
"tray.quickChat": "快捷聊天",
"tray.quit": "退出",
"tray.show": "显示 {{appName}}",
"view.forceReload": "强制重新加载",
@@ -86,5 +86,6 @@
"window.minimize": "最小化",
"window.title": "視窗",
"window.toggleFullscreen": "切換全螢幕",
"window.zoom": "縮放"
"window.zoom": "縮放",
"tray.openMiniToolbar": "Quick Composer"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Generate the macOS tray template icon set (black + alpha).
*
* Template images must contain only black pixels and an alpha channel;
* macOS then recolors them automatically based on the menu bar theme.
*
* Renders two files in apps/desktop/resources:
* - trayTemplate.png (@1x, 18x18)
* - trayTemplate@2x.png (@2x, 36x36)
*
* Run: bun run apps/desktop/scripts/generate-tray-template.mjs
*/
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.resolve(__dirname, '..', 'resources');
// Silhouette derived from the LobeHub logo. Eyes and mouth are cut as
// transparent holes via fill-rule=evenodd so they remain visible when
// macOS tints the entire shape in a single color.
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320">
<path fill="#000" d="M172.997 19.016c-14.027 0-19.5-11.5-41-11-23.394 0-34 13-45.5 23-1.958 1.702-11.5 7-16 9-19.683 8.748-34.5 21.5-34.5 40.5 0 20.711 17.461 37.5 39 37.5 3.536 0 6.963-.453 10.22-1.301 8.7 10.539 22.179 16.658 37.28 17.301 23.5 1 31-15.25 44.5-8.5 9.259 4.629 13.83 8.5 28.5 8.5 17.108 0 25.057-5.233 30-11 9-10.5 22.879-4 31.5-4 18.778 0 34-14.551 34-32.5 0-17.95-15.222-32.5-34-32.5-5.15 0-14.856 1.27-17-7-3.5-13.5-20.148-29-44-29-9.318 0-17.691 1-23 1z"/>
<path fill="#000" fill-rule="evenodd" d="M294 172.519c0 75.655-59.442 128.5-134 128.5-74.558 0-134-53.845-134-129.5 0-22.5 5-32.141 31.5-35.671 47.5-6.329 72.542-3.829 102.5-3.829 29.959 0 72.556-1.27 102.5 3.829 24.5 4.171 30 8.671 31.5 36.671zM101 221.012c15.464 0 28-12.536 28-28s-12.536-28-28-28-28 12.536-28 28 12.536 28 28 28zM219 221.012c15.464 0 28-12.536 28-28s-12.536-28-28-28-28 12.536-28 28 12.536 28 28 28zM159.75 242.51c-28.25 0-35.75 3.5-35.75 3.5s3.5 27 35.75 27 35.75-27 35.75-27-7.5-3.5-35.75-3.5z"/>
</svg>
`;
async function render(size, outFile) {
const buf = Buffer.from(svg);
await sharp(buf, { density: Math.max(72, size * 12) })
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toFile(outFile);
console.log(`wrote ${path.relative(process.cwd(), outFile)} (${size}x${size})`);
}
async function main() {
await mkdir(outDir, { recursive: true });
await render(18, path.join(outDir, 'trayTemplate.png'));
await render(36, path.join(outDir, 'trayTemplate@2x.png'));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+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;
+10 -10
View File
@@ -1,29 +1,29 @@
import { join } from 'node:path';
import path from 'node:path';
import { app } from 'electron';
export const mainDir = join(__dirname);
export const mainDir = path.join(__dirname);
export const preloadDir = join(mainDir, '../preload');
export const preloadDir = path.join(mainDir, '../preload');
export const resourcesDir = join(mainDir, '../../resources');
export const resourcesDir = path.join(mainDir, '../../resources');
export const buildDir = join(mainDir, '../../build');
export const buildDir = path.join(mainDir, '../../build');
export const binDir = app.isPackaged
? join(process.resourcesPath, 'bin')
: join(resourcesDir, 'bin');
? path.join(process.resourcesPath, 'bin')
: path.join(resourcesDir, 'bin');
const appPath = app.getAppPath();
export const rendererDir = join(appPath, 'dist', 'renderer');
export const rendererDir = path.join(appPath, 'dist', 'renderer');
export const userDataDir = app.getPath('userData');
export const appStorageDir = join(userDataDir, 'lobehub-storage');
export const appStorageDir = path.join(userDataDir, 'lobehub-storage');
// Legacy local database directory used in older desktop versions
export const legacyLocalDbDir = join(appStorageDir, 'lobehub-local-db');
export const legacyLocalDbDir = path.join(appStorageDir, 'lobehub-local-db');
// ------ Application storage directory ---- //
+5 -5
View File
@@ -1,16 +1,16 @@
import os from 'node:os';
import { dev, linux, macOS, windows } from 'electron-is';
import * as electronIs from 'electron-is';
import { getDesktopEnv } from '@/env';
export const isDev = dev();
export const isDev = electronIs.dev();
export const OFFICIAL_CLOUD_SERVER = getDesktopEnv().OFFICIAL_CLOUD_SERVER;
export const isMac = macOS();
export const isWindows = windows();
export const isLinux = linux();
export const isMac = electronIs.macOS();
export const isWindows = electronIs.windows();
export const isLinux = electronIs.linux();
function getIsMacTahoe(): boolean {
if (!isMac) return false;
+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,
+4 -3
View File
@@ -12,6 +12,7 @@ import { BrowserWindow, shell } from 'electron';
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
import { appendVercelCookie } from '@/utils/http-headers';
import { createLogger } from '@/utils/logger';
import { netFetch } from '@/utils/net-fetch';
import { ControllerModule, IpcMethod } from './index';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
@@ -360,10 +361,10 @@ export default class AuthCtr extends ControllerModule {
logger.debug(`Polling for credentials: ${url.toString()}`);
// Send HTTP request directly
// Use Electron net.fetch to respect system CA store (self-signed/private CA certs)
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
appendVercelCookie(headers);
const response = await fetch(url.toString(), { headers, method: 'GET' });
const response = await netFetch(url.toString(), { headers, method: 'GET' });
// Check response status
if (response.status === 404) {
@@ -481,7 +482,7 @@ export default class AuthCtr extends ControllerModule {
'Content-Type': 'application/x-www-form-urlencoded',
};
appendVercelCookie(tokenHeaders);
const response = await fetch(tokenUrl.toString(), {
const response = await netFetch(tokenUrl.toString(), {
body,
headers: tokenHeaders,
method: 'POST',
@@ -1,4 +1,5 @@
import type {
FocusTopicPopupParams,
InterceptRouteParams,
OpenSettingsWindowOptions,
WindowMinimumSizeParams,
@@ -15,11 +16,21 @@ export default class BrowserWindowsCtr extends ControllerModule {
static override readonly groupName = 'windows';
@shortcut('showApp')
async toggleMainWindow() {
toggleMainWindow() {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.toggleVisible();
}
@shortcut('quickComposer')
async openQuickComposer() {
await this.app.screenCaptureManager.startSession();
}
@shortcut('quickChat')
openQuickChat() {
this.app.browserManager.openQuickChatPopup();
}
@IpcMethod()
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
const normalizedOptions: OpenSettingsWindowOptions =
@@ -80,6 +91,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) => {
@@ -0,0 +1,58 @@
import { exec } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { promisify } from 'node:util';
import { getCliWrapperDir } from '@/modules/cliEmbedding';
import { createLogger } from '@/utils/logger';
import { ControllerModule, IpcMethod } from './index';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
const logger = createLogger('controllers:CliCtr');
function normalizeServerUrl(url: string): string {
return url.replace(/\/$/, '');
}
export default class CliCtr extends ControllerModule {
static override readonly groupName = 'cli';
@IpcMethod()
async runCliCommand(args: string): Promise<{ exitCode: number; stderr: string; stdout: string }> {
const execAsync = promisify(exec);
const wrapperDir = getCliWrapperDir();
const cmd = process.platform === 'win32' ? 'lobehub.cmd' : 'lobehub';
const wrapperPath = path.join(wrapperDir, cmd);
const env = { ...process.env };
const remoteCtr = this.app.getController(RemoteServerConfigCtr);
if (remoteCtr) {
const [token, serverUrl] = await Promise.all([
remoteCtr.getAccessToken(),
remoteCtr.getRemoteServerUrl(),
]);
if (token && serverUrl) {
env.LOBEHUB_JWT = token;
env.LOBEHUB_SERVER = normalizeServerUrl(serverUrl);
logger.debug('Injected LOBEHUB_JWT / LOBEHUB_SERVER for CLI command');
}
}
try {
const { stdout, stderr } = await execAsync(`"${wrapperPath}" ${args}`, {
env,
timeout: 15_000,
});
return { exitCode: 0, stderr, stdout };
} catch (error: any) {
return {
exitCode: error.code ?? 1,
stderr: error.stderr ?? '',
stdout: error.stdout ?? String(error.message),
};
}
}
}
+420
View File
@@ -0,0 +1,420 @@
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,
GitPullResult,
GitPushResult,
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).
*
* Does a best-effort `git fetch` first so the result reflects what's
* actually on the remote the renderer calls this via SWR with
* `revalidateOnFocus`, so the fetch piggybacks on window re-focus. Fetch
* failures (offline, no credentials, no `origin` remote) are swallowed so
* we still return whatever can be computed against the cached refs.
*/
@IpcMethod()
async getGitAheadBehind(dirPath: string): Promise<GitAheadBehind> {
const execFileAsync = promisify(execFile);
try {
await execFileAsync('git', ['fetch', '--no-tags', '--quiet', 'origin'], {
cwd: dirPath,
timeout: 10_000,
});
} catch {
// swallow — fall through to compute against cached refs
}
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;
// `git push -u origin HEAD` always targets origin/<current-branch-name>,
// which may differ from upstream (the branched-off-canary case).
let pushTarget: string | undefined;
let pushTargetExists = false;
try {
const { stdout: branchOut } = await execFileAsync(
'git',
['symbolic-ref', '--short', 'HEAD'],
{ cwd: dirPath, timeout: 5000 },
);
const branch = branchOut.trim();
if (branch) {
pushTarget = `origin/${branch}`;
try {
await execFileAsync(
'git',
['rev-parse', '--verify', '--quiet', `refs/remotes/${pushTarget}`],
{ cwd: dirPath, timeout: 5000 },
);
pushTargetExists = true;
} catch {
pushTargetExists = false;
}
}
} catch {
// detached HEAD — leave pushTarget undefined
}
return { ahead, behind, hasUpstream: true, pushTarget, pushTargetExists, 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 };
}
}
/**
* Pull the current branch's upstream via fast-forward only.
*
* `--ff-only` avoids creating accidental merge commits when the local branch
* has diverged in that case the user should resolve merge/rebase in their
* own terminal. For the common "just behind" case this is a safe one-click.
*/
@IpcMethod()
async pullGitBranch(payload: { path: string }): Promise<GitPullResult> {
const { path: dirPath } = payload;
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync('git', ['pull', '--ff-only'], {
cwd: dirPath,
timeout: 60_000,
});
const noop = /Already up to date/i.test(stdout);
return { noop, success: true };
} catch (error: any) {
const stderr: string = (error?.stderr ?? error?.message ?? '').toString().trim();
logger.debug('[pullGitBranch] failed', { stderr });
return { error: stderr || 'git pull failed', success: false };
}
}
/**
* Push the current branch to its same-named remote on `origin`.
*
* Uses `git push -u origin HEAD` instead of plain `git push` so the action
* works even when local branch name differs from the configured upstream
*/
@IpcMethod()
async pushGitBranch(payload: { path: string }): Promise<GitPushResult> {
const { path: dirPath } = payload;
const execFileAsync = promisify(execFile);
try {
const { stderr } = await execFileAsync('git', ['push', '-u', 'origin', 'HEAD'], {
cwd: dirPath,
timeout: 60_000,
});
// git push writes progress/status to stderr even on success
const noop = /Everything up-to-date/i.test(stderr);
return { noop, success: true };
} catch (error: any) {
const stderr: string = (error?.stderr ?? error?.message ?? '').toString().trim();
logger.debug('[pushGitBranch] failed', { stderr });
return { error: stderr || 'git push 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();
});
}
}
@@ -48,6 +48,7 @@ import { type FileResult, type SearchOptions } from '@/modules/fileSearch';
import ContentSearchService from '@/services/contentSearchSrv';
import FileSearchService from '@/services/fileSearchSrv';
import { createLogger } from '@/utils/logger';
import { netFetch } from '@/utils/net-fetch';
import { ControllerModule, IpcMethod } from './index';
@@ -341,7 +342,7 @@ export default class LocalFileCtr extends ControllerModule {
}
try {
const response = await fetch(url);
const response = await netFetch(url);
if (!response.ok) {
throw new Error(
`Failed to download skill package: ${response.status} ${response.statusText}`,
@@ -4,12 +4,11 @@ import { isEqual, merge } from 'es-toolkit/compat';
import { defaultProxySettings } from '@/const/store';
import { createLogger } from '@/utils/logger';
import type {
ProxyTestResult} from '../modules/networkProxy';
import type { ProxyTestResult } from '../modules/networkProxy';
import {
ProxyConfigValidator,
ProxyConnectionTester,
ProxyDispatcherManager
ProxyDispatcherManager,
} from '../modules/networkProxy';
import { ControllerModule, IpcMethod } from './index';
@@ -104,7 +103,7 @@ export default class NetworkProxyCtr extends ControllerModule {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Proxy connection test failed:', errorMessage);
throw new Error(`Connection failed: ${errorMessage}`);
throw new Error(`Connection failed: ${errorMessage}`, { cause: error });
}
}
@@ -3,7 +3,7 @@ import type {
ShowDesktopNotificationParams,
} from '@lobechat/electron-client-ipc';
import { app, Notification } from 'electron';
import { macOS, windows } from 'electron-is';
import * as electronIs from 'electron-is';
import { getIpcContext } from '@/utils/ipc';
import { createLogger } from '@/utils/logger';
@@ -20,7 +20,7 @@ export default class NotificationCtr extends ControllerModule {
if (!Notification.isSupported()) return 'denied';
// Keep a stable status string for renderer-side UI mapping.
// Screen3 expects macOS to return 'authorized' when granted.
if (!macOS()) return 'authorized';
if (!electronIs.macOS()) return 'authorized';
// Electron 38 no longer exposes `systemPreferences.getNotificationSettings()` in types,
// and some runtimes don't provide it at all. Use the renderer's Notification.permission
@@ -43,7 +43,7 @@ export default class NotificationCtr extends ControllerModule {
// On macOS, ask permission via Web Notification API first when possible.
// This helps keep `Notification.permission` in sync for subsequent status checks.
if (macOS()) {
if (electronIs.macOS()) {
try {
const mainWindow = this.app.browserManager.getMainWindow().browserWindow;
await mainWindow.webContents.executeJavaScript('Notification.requestPermission()', true);
@@ -83,12 +83,12 @@ export default class NotificationCtr extends ControllerModule {
}
// On macOS, we may need to explicitly request notification permissions
if (macOS()) {
if (electronIs.macOS()) {
logger.debug('macOS detected, notification permissions should be handled by system');
}
// Set app user model ID on Windows
if (windows()) {
if (electronIs.windows()) {
app.setAppUserModelId('com.lobehub.chat');
logger.debug('Set Windows App User Model ID for notifications');
}
@@ -131,7 +131,12 @@ export default class NotificationCtr extends ControllerModule {
silent: params.silent || false,
timeoutType: 'default',
title: params.title,
urgency: 'normal',
// On Linux/GNOME Shell, urgency 'normal' causes notifications to appear as banners.
// Clicking the dismiss (X) button on such banners can freeze the system for 30-45 seconds
// due to heavy gnome-shell processing. Using 'low' urgency routes notifications to the
// message tray instead, preventing the banner's X button from being shown.
// The urgency option is ignored on macOS and Windows.
urgency: electronIs.linux() ? 'low' : 'normal',
});
// Add more event listeners for debugging
@@ -173,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 (electronIs.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
*/
@@ -9,6 +9,7 @@ import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
import GatewayConnectionService from '@/services/gatewayConnectionSrv';
import { appendVercelCookie } from '@/utils/http-headers';
import { createLogger } from '@/utils/logger';
import { netFetch } from '@/utils/net-fetch';
import { ControllerModule, IpcMethod } from './index';
@@ -485,7 +486,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
'Content-Type': 'application/x-www-form-urlencoded',
};
appendVercelCookie(headers);
const response = await fetch(tokenUrl.toString(), { body, headers, method: 'POST' });
const response = await netFetch(tokenUrl.toString(), { body, headers, method: 'POST' });
if (!response.ok) {
// Try to parse error response
@@ -0,0 +1,72 @@
import type {
CapturePreviewResult,
CaptureRectParams,
OverlayCaptureUploadStatusPayload,
ScreenCaptureSubmitParams,
} from '@lobechat/electron-client-ipc';
import type { OverlaySnapshotPayload } from '@/modules/screenCapture/ScreenCaptureManager';
import { createLogger } from '@/utils/logger';
import { ControllerModule, IpcMethod } from './index';
const logger = createLogger('controllers:ScreenCaptureCtr');
export default class ScreenCaptureCtr extends ControllerModule {
static override readonly groupName = 'screenCapture';
@IpcMethod()
async traceOverlayEvent(payload: { data?: unknown; event: string }): Promise<void> {
console.info('[screenCapture:overlay]', payload.event, payload.data ?? '');
}
@IpcMethod()
async previewWindow(windowId: number): Promise<CapturePreviewResult> {
logger.debug(`previewWindow request: ${windowId}`);
return this.app.screenCaptureManager.handlePreviewWindow(windowId);
}
@IpcMethod()
async previewRect(params: CaptureRectParams): Promise<CapturePreviewResult> {
logger.debug(`previewRect request: ${JSON.stringify(params)}`);
return this.app.screenCaptureManager.handlePreviewRect(params);
}
@IpcMethod()
async submit(params: ScreenCaptureSubmitParams): Promise<void> {
logger.debug(`submit request: prompt-len=${params.prompt.length}`);
await this.app.screenCaptureManager.handleSubmit(params);
}
/**
* Status update reported by the main renderer after it finishes (or fails)
* uploading a capture's bytes. Forwarded to the overlay to drive the send
* button's enabled state.
*/
@IpcMethod()
async reportUploadStatus(payload: OverlayCaptureUploadStatusPayload): Promise<void> {
logger.debug(
`reportUploadStatus captureId=${payload.captureId} status=${payload.status} fileId=${payload.fileId ?? '-'}`,
);
this.app.screenCaptureManager.reportUploadStatus(payload);
}
@IpcMethod()
async close(): Promise<void> {
logger.debug('close overlay request');
this.app.screenCaptureManager.close();
}
/**
* Renderer-driven snapshot of agents/models for the overlay selector. The
* main renderer pushes this whenever its data layer (TRPC stores) reports
* a change; main process only caches and forwards it does not fetch.
*/
@IpcMethod()
async publishOverlaySnapshot(payload: OverlaySnapshotPayload): Promise<void> {
logger.debug(
`publishOverlaySnapshot — agents=${payload.agents?.length ?? 0} models=${payload.models?.length ?? 0}`,
);
this.app.screenCaptureManager.publishOverlaySnapshot(payload);
}
}
@@ -10,17 +10,38 @@ import { runCommand, ShellProcessManager } from '@lobechat/local-file-shell';
import { createLogger } from '@/utils/logger';
import CliCtr from './CliCtr';
import { ControllerModule, IpcMethod } from './index';
const logger = createLogger('controllers:ShellCommandCtr');
const processManager = new ShellProcessManager();
/** Prefix for a simple `lh`/`lobe`/`lobehub` invocation (keyword + boundary, args via slice). */
const SIMPLE_LH_PREFIX = /^\s*(?:lh|lobe|lobehub)(?=\s|$)/;
export default class ShellCommandCtr extends ControllerModule {
static override readonly groupName = 'shellCommand';
@IpcMethod()
async handleRunCommand(params: RunCommandParams): Promise<RunCommandResult> {
const prefixMatch = SIMPLE_LH_PREFIX.exec(params.command);
if (prefixMatch) {
const cliCtr = this.app.getController(CliCtr);
if (cliCtr) {
const args = params.command.slice(prefixMatch[0].length).trim();
logger.debug('Routing lh command to CliCtr.runCliCommand:', args);
const result = await cliCtr.runCliCommand(args);
return {
exit_code: result.exitCode,
output: result.stdout + result.stderr,
stderr: result.stderr,
stdout: result.stdout,
success: result.exitCode === 0,
};
}
}
return runCommand(params, { logger, processManager });
}

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