* ⚡️ perf(build): remove sitemap generation to cut static export time
The sitemap accounted for 772 of 827 prerendered pages, each fetching
marketplace data at build time. Static generation drops from 28.2s to
0.3s and total next build from ~59s to ~32s.
* Redirect legacy sitemap URLs to the landing site
* Redirect sitemap index to landing sitemap
* ✨ feat: add delete/uninstall actions to settings/skill items
- LobehubSkillItem: show compact `...` dropdown in list mode for connected items with Disconnect action (revokes OAuth)
- KlavisSkillItem: show compact `...` dropdown in list mode for connected/pending servers with Remove action (true delete via removeKlavisServer)
- ConnectorDetail: add Delete button for custom (mcp) connectors; calls deleteConnector + notifies parent via onDelete
- SkillDetail / Page: thread onDelete callback so selecting null after deletion triggers auto-select of next item
- Locales: add tools.klavis.remove / removeConfirm.title / removeConfirm.desc in en-US, zh-CN, and default source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(skill): gate Klavis remove by canEdit and clear selected after removal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(skill): show dropdown for all Klavis/Lobehub items in list mode
Previously, the ... button was gated behind `server` (Klavis) and
`isConnected` (LobehubSkill), so disconnected/never-connected items
showed no actions. Remove those guards so the dropdown always renders
in list mode. handleRemove/handleDisconnect now skip the server call
when no server instance exists and instead clear the selected item.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(skill): move delete/uninstall actions from list dropdown to detail panel
- Remove heavy ... dropdown from KlavisSkillItem / LobehubSkillItem list items
- Add danger Uninstall button to builtin-skill detail header (matches ConnectorDetail style)
- Add slim action bar with Uninstall to agent-skill detail panel
- All actions respect canEdit / canCreate permissions with confirmModal gating
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: activator tool discovery for cloud-sandbox and local-system
- P0: Explicitly inject LocalSystemManifest when device gateway is configured
(discoverable: isDesktop is always false on server, so it never enters
the discovery loop. The explicit injection mirrors the canUseDevice guard.)
- P1: Skip CloudSandboxManifest when runtimeMode is not 'cloud'
(resolveRuntimeMode unifies executionTarget='sandbox' and legacy
chatConfig.runtimeEnv.runtimeMode paths, so agents with sandbox
disabled correctly exclude the cloud-sandbox tool.)
Both fixes operate at the manifest-map build stage, consistently affecting
all downstream consumers (activator discovery, availableTools, etc.)
* 🐛 fix: remove cloud-sandbox manifest when runtime is not sandbox
The initial manifest seed via getEnabledPluginManifests includes
defaultToolIds (which contains lobe-cloud-sandbox), so the manifest
was already in toolManifestMap before the allowedBuiltinTools loop's
continue guard. This made lobe-cloud-sandbox activatable even when
sandbox was disabled.
Add a delete right after resolveRuntimeMode to cover both the
manifestMap seed and the allowedBuiltinTools loop in one place.
Co-authored-by: chatgpt-codex-connector[bot]
* ♻️ refactor: replace Segmented tabs with SearchBar in ProfileEditor tool dropdown
- PopoverContent: replace Segmented with SearchBar + internal client-side filtering (same pattern as ChatInput ActionBar)
- AgentTool: remove ~270 lines of duplicated installedTabItems useMemo; pass unified items
- AgentTool: add auto-cleanup for stale plugin identifiers in agent config
* 🐛 fix(agent): persist file attachments in hetero early-exit user message
The hetero-agent early exit in execAgent created the user message without
the `files` relation, so attachments sent from the SPA gateway path
(executionTarget=device / sandbox) were never linked via messagesFiles and
disappeared once the optimistic client message was replaced by the server
snapshot. Attach the deduped `fileIds` the same way sendMessageInServer
does on the local-mode path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(agent): deliver image attachments to device/sandbox hetero runs
Persisting the messagesFiles relation fixed display, but the dispatched
CLI still never saw the image — local mode feeds the persisted imageList
into sendPrompt for vision, while the device/sandbox dispatch protocols
(agent_run_request / sandbox runner) only carried a text prompt.
- resolve attached images into signed URLs in the hetero early exit
(metadata-only, non-fatal) and carry them through heteroParams
- add imageList to the agent_run_request wire type and dispatchAgentRun
params (gateway client + server service)
- extract buildHeteroExecStdinPayload into @lobechat/heterogeneous-agents
so the three dispatch sites (desktop spawnLhHeteroExec, lh connect
daemon, server sandbox runner) build the same content-block payload:
systemContext, prompt, then image blocks
- lh hetero exec already coerces image blocks via coerceJsonPrompt and
normalizeImage (url → base64 for Claude Code, materialized path for
Codex), so no CLI consumer changes are needed
openclaw/hermes (runHeteroTask) keep text-only prompts — their dispatch
goes through a separate one-shot tool protocol.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(heterogeneous-agents): move exec stdin wire contract to a pure /protocol entry
The server sandbox runner imported `buildHeteroExecStdinPayload` through the
`/spawn` barrel, which (with no `sideEffects` hint) bundles the whole spawn
machinery into the Next.js server chunk. Its `process.cwd()`-rooted dynamic
fs calls then make Vercel's output file tracing glob the entire repo source
tree into every serverless function (+~69 MB each), pushing the 4 largest
functions past the 250 MB uncompressed limit and failing the deployment.
Split the dispatch wire contract (stdin payload builder + content-block
types) into a new pure, isomorphic `/protocol` export and point all three
dispatch sites (server sandbox runner, desktop main, `lh connect` daemon) at
it. `/spawn` re-exports the moved symbols so executor-side callers are
unaffected. Also declare `sideEffects: false` for the package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agentDocument): listDocuments returns templateId and derived fields
* fix(agentDocument): useFetchAgentDocuments use listDocuments instead of getDocuments
* fix(agentDocument): derive AgentDocumentItem from listDocuments return type
* fix(agentDocument): export AgentDocumentListItem type
* 🐛 fix(agentDocument): align list projections and consumers after rebase onto canary
- listDocumentsForTopic now returns the same projection as listDocuments
(derived fields + templateId), so the tRPC union no longer collapses
the inferred client type to the old 8-field shape
- add description/updatedAt to both projections for sidebar consumers
- AgentDocumentsGroup switches getDocuments -> listDocuments (it already
shared the documentsList SWR key)
- makePendingDocument trimmed to the lean list item shape
- update useFetchAgentDocuments test to the listDocuments behavior
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agentDocument): migrate agentDocumentSkills sync to slim listDocuments
The tool store's skill registry sync shared agentDocumentSWRKeys.documentsList
with the working sidebar and the new useFetchAgentDocuments hook, but still
fetched the full getDocuments payload. Sharing one SWR key across different
payload shapes made the cached result order-dependent: whichever consumer
mounted first decided whether the cache held the heavy full documents or the
slim list items. Migrate the skills sync to listDocuments, whose projection
covers every field mapDocsToSkills reads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(agent): single-track device-tool injection via execution plan
P3 follow-up to #15669 — downstream layers now consume the resolved
ExecutionPlan instead of re-deriving device capability:
- ExecutionPlan carries the effective `target`; persisted into
state.metadata.executionPlan via createOperation
- call_llm executor gates buildStepToolDelta's activeDeviceId signal on
the plan (none/sandbox can never re-inject local-system mid-run)
- AgentToolsEngine consumes the plan's target; redundant rule-level
canUseDevice checks removed (physical manifest walls remain)
- builtin agent runtime config can now override agencyConfig
(web-onboarding pins executionTarget=none)
- hetero desktop 'local' selection persists this desktop's deviceId so
opening the agent from web dispatches to the same machine via gateway
- 'local' vs 'device' stay distinct user choices even for the same
machine: gateway dispatch streams progress to all clients (mobile),
IPC is faster but desktop-session-only — guarded by a regression test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): enforce device access policy on hetero dispatch
resolveDeviceAccessPolicy now runs BEFORE the hetero early exit and feeds
canUseDevice into the hetero execution plan: a denied sender (external
bot user) degrades local/device-bound CLI hetero runs to the cloud
sandbox instead of dispatching to the owner's machine, and requestedDeviceId
cannot bypass the policy. Remote hetero agents (openclaw/hermes) are
device-only with no sandbox fallback, so denied senders are refused
outright.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(agent): fix interface field order in RuntimeSelectionContext
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent-runtime): always persist assistant reasoning to DB
PR #13494 gated message reasoning persistence behind preserveThinking
(agent chatConfig + model extendParams / qwen|zhipu fallback). That gate
is only meant to control whether reasoning is replayed into the next LLM
payload — applying it to the DB write dropped thinking content for every
non-qwen/zhipu reasoning model in server-side agent mode: reasoning
streamed live via stream_end but vanished after refresh.
Restore unconditional reasoning persistence in messageModel.update and
keep the preserveThinking gate only for state.messages payload replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(i18n): localize callSubAgent tool labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✅ test(model-runtime): align tool-calling fallback tests with new return shape
#15680 changed generateObject's tool-calling fallback to return the parsed
schema object (same shape as the json_schema path) instead of an array of
tool calls, and reworked its error handling, but left the pre-existing
"tool calling fallback" block in index.test.ts asserting the old behavior,
breaking CI on canary:
- result is now the parsed object, not [{ name, arguments }]
- the no-tool-call path returns undefined via debug log without console.error
- the parse-failure path logs the single matched tool call, not the array
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-runtime): surface missing tool call in generateObject fallback as error
tool_choice forces the structured-output function, so a response without a
tool call means the provider misbehaved. #15680 routed this branch to a
debug-namespace log that is invisible in production, leaving callers with
an unexplained undefined. Log it via console.error with the response
message as context, matching the parse-failure branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat: add browser device pairing flow to /settings/devices
- Add "Via Browser" tab to ConnectDeviceModal with pairing code display and input
- Add "Register this browser as a device" callout card above DeviceList
- Support ?pair=<code> URL param to auto-open browser pairing modal with pre-filled code
- Improve DeviceList empty state with method cards (Desktop + CLI)
- Ship en-US and zh-CN i18n keys for all new browser/sync strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔨 fix(devices): fix lint warnings — import sort order and empty catch block
* fix(devices): add pair API route and invalidate device list cache
- Create /api/devices/pair POST handler that authenticates the user via
Better Auth session, validates the code against the user's registered
devices via DeviceModel.findByDeviceId, and returns JSON.
- Replace the setListKey/key-prop re-mount trick with
lambdaQuery.useUtils().device.listDevices.invalidate() so the tRPC
React Query cache is properly busted after a successful pair (fixes
staleTime: 30s preventing the new device from appearing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ♻️ refactor(devices): drop browser pairing, fix modal close, redesign UI
- Remove the "Via Browser" pairing flow entirely: browser tab in
ConnectDeviceModal, the "register this browser" callout card, the
?pair=<code> deep-link, and the /api/devices/pair stub route. Only the
real Desktop and CLI connection methods remain.
- Fix the modal that couldn't be closed: @lobehub/ui Modal closes via
onCancel (antd), not onClose — the X button was a no-op.
- Redesign the connect modal (segmented tabs, numbered steps, command
blocks with copy, security footer) and the empty state (onboarding
hero with Desktop/CLI options + capability cards).
- Clean up browser/sync i18n keys; add capabilities + footer keys for
en-US and zh-CN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 💄 fix(devices): apply card radius — cssVar.borderRadius already has unit
The radius tokens (cssVar.borderRadius / borderRadiusLG) already include
their unit, so the trailing `px` produced `var(--…)px`, which browsers
drop — leaving the cards with sharp corners. Drop the `px` so the cards
pick up the same rounded radius as the appearance settings FormGroup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- add resolveExecutionPlan as THE device decision (none/sandbox never
route to a device; offline bindings stay unrouted; single-online-device
auto-activation only for device-capable targets)
- fix executionTarget=none being bypassed by single-device auto-activation
(background runs executed device tools despite 无设备)
- stop exposing the remote-device proxy in none/sandbox sessions
- converge native execAgent, hetero dispatch fork and client
selectRuntimeType onto the shared resolution
- drop the legacy per-platform chatConfig.runtimeEnv.runtimeMode fallback
entirely (no migration: unset targets resolve to platform defaults)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- actions/checkout@v4 -> @v6 in issue-auto-comments.yml
(last remaining @v4 usage; all other 48 uses are already @v6)
- actions/github-script@v7 -> @v8 in release-desktop-canary.yml
(last remaining @v7 usage; all other 4 uses are already @v8)
Co-authored-by: 章岚 <zhanglan@datagrand.com>
* ✨ feat(model-bank): backfill knowledgeCutoff batch 2 and restore lost Anthropic values
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 📝 docs(skills): add model-bank-metadata skill for cutoff/family backfill
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-bank): Claude Fable 5 belongs to the claude-mythos family
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(desktop): always surface the tab bar by creating a tab on first navigation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ♻️ refactor(model-bank): family is the product lineage (claude-opus/sonnet/haiku), not the brand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(agent): backfill activeAgentId before paint on tab/route switches
Tab switches are plain route navigations, so leaving an agent page cleared
activeAgentId via a passive useUnmount and the next page re-set it in a
passive useEffect — the first painted frame always had no active id, flashing
a skeleton even when agentMap already cached the config. Move both the
backfill and the unmount clear to layout effects: removed-tree layout
cleanups run before new-tree layout effects in one commit, so the clear can
never wipe a freshly synced id and the id is in place before paint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(agent): surface agent config fetch errors with a retry action
isAgentConfigLoading only knows "no data yet", so a failed fetch (e.g. a 401
that SWR deliberately does not retry, with no focus revalidation inside a
single Electron window) left the agent page on a skeleton forever — only a
manual reload recovered. Record per-agent fetch errors in
agentConfigErrorMap (set by onError, cleared on data / retry), expose
currentAgentConfigError / isAgentConfigError selectors, add a
retryAgentConfigFetch action that revalidates the agent's SWR entries, and
show an error alert with a retry button above the main chat input while the
config is still missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(ci): sync model metadata test expectations
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(connector): support API key / custom header / OAuth auth in custom connector
Make the connector backend a full replacement for the legacy custom-MCP plugin form:
- connector create/update now accept bearer/apikey/header credentials (encrypted at rest);
oauth2 stays callback-only
- map apikey → bearer auth and header → request headers in both the sync path
(syncTools + callTool) and the agent-runtime manifest path
- pass custom HTTP headers through to the MCP client
- AddConnectorModal becomes a rich form: MCP type (HTTP/STDIO), auth type
(None / API Key / Custom Headers / OAuth), reusing the plugin form inputs;
OAuth keeps the existing popup authorize flow, others create + sync directly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(connector): fold OAuth into the PluginDevModal MCP form
Pivot the custom-MCP entry to reuse the rich PluginDevModal / MCPManifestForm
instead of a bespoke connector modal, and add OAuth as an auth type inside it:
- MCPManifestForm: gated `enableOAuth` adds an "OAuth" auth type with
Client ID / Secret (optional) + redirect-URI hint. Only the custom-connector
entry enables it, so plain custom-plugin DevModal callers (editing plugins,
agent tools, …) are unaffected.
- DevModal: opens the OAuth popup synchronously on the save click (browsers
block window.open once an async boundary is crossed), validates, then hands
the popup to onSave which navigates it to the authorize URL.
- New CustomConnectorModal wraps DevModal and persists every auth type onto the
connector backend (none / bearer / custom headers → create + sync; OAuth →
create with OIDC config + run the authorize popup).
- settings/skill entry now opens CustomConnectorModal; the standalone
AddConnectorModal rich rewrite from the previous commit is reverted to the
canary original (it is only referenced by the unused ConnectorList).
- i18n: dev.mcp.auth.oauth* keys (default + en-US + zh-CN).
Backend stays as in the prior commit (connector create/update accept
bearer/apikey/header credentials; sync + manifest paths apply them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(connector): route the OAuth auth type through the authorize flow, not the token-less manifest test
Selecting OAuth and clicking "Test connection" called the plugin manifest test
(getStreamableMcpServerManifest), which connects with no token and 401s on any
OAuth-gated server (e.g. Linear MCP / DCR). For OAuth there is nothing to test
without authorizing first, so the button now becomes "Authorize & Connect" and
runs the connector OAuth flow (discovery + DCR + authorize popup), shared with
the footer save button via DevModal.runOAuthFlow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* 🐛 fix(connector): make connector.create idempotent on (user, identifier)
Re-adding or re-authorizing a custom connector with an existing identifier hit
the user_connectors unique constraint and 500'd. Now an existing row is updated
(reset to disconnected, refreshed name/url/oidcConfig/credentials) and its id
reused, instead of inserting a duplicate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ♻️ refactor(skill-store): route Add Custom MCP through the connector modal, drop the Custom tab
- Skill Store "Add → Add Custom MCP Skill" now opens CustomConnectorModal
(connector backend + OAuth), matching the settings/skill entry, instead of
the legacy plugin DevModal (installCustomPlugin + togglePlugin).
- Remove the now-redundant "Custom" tab from the Skill Store (custom MCP lives
in the connector list now): drop SkillStoreTab.Custom, its tab option,
CustomList render, and the matching search branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ feat(model-bank): backfill knowledgeCutoff for OpenAI/Claude/Llama/Phi families (batch 1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add family/generation fields with rule-derived data for chat models
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add canonical knowledge-cutoff map with build-time backfill
Adds MODEL_KNOWLEDGE_CUTOFFS (canonical id → YYYY-MM, all values verified
against official provider docs) plus normalizeModelIdForCutoff, which reduces
provider-specific spellings (openrouter/bedrock prefixes, dated snapshots,
-thinking/-fast/-latest/-preview variants, claude dot-versions) to canonical
ids. buildDefaultModelList backfills knowledgeCutoff from the map when a model
card has no inline value, so all aggregator providers inherit cutoffs
automatically; inline values always win.
Covers Anthropic (incl. legacy 3.x), OpenAI, Google Gemini/Gemma, xAI Grok,
Meta Llama, Amazon Nova, and Cohere. DeepSeek/Qwen/GLM/Kimi/MiniMax/Mistral
publish no official cutoffs and are intentionally absent. Anthropic inline
PoC entries migrate into the map (single source of truth).
Cross-checked against the batch-1 inline backfill: 0 value mismatches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(model-bank): correct Claude Sonnet 4.6 cutoff
* ✅ test(model-bank): sync metadata expectations
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add knowledgeCutoff field with Anthropic models as PoC
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(model-bank): add family/generation fields to model card types
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>