* 💄 style: shrink desktop header icons and tighten sidebar/home density
Switches all desktop header action icons from DESKTOP_HEADER_ICON_SIZE to
DESKTOP_HEADER_ICON_SMALL_SIZE, and tightens vertical gaps in the home
sidebar, recents list, and nav header layout for a denser, calmer look.
* ♻️ refactor(agent-tasks): migrate task menus and scheduler select to @lobehub/ui base-ui
- TaskPriorityTag / TaskStatusTag: replace antd Dropdown with base-ui
DropdownMenu and adopt the ContextMenuItem / MenuInfo typings.
- useTaskItemContextMenu: drop the DOM data-attribute submenu marker in
favour of an internal activeSubmenuRef tracked via onOpenChange.
- TaskScheduleConfig / SchedulerForm: swap @lobehub/ui Select for the
base-ui Select and replace the custom SearchBar dropdownRender with
antd Select showSearch for timezone filtering.
* ♻️ refactor(review): migrate review dropdowns to @lobehub/ui base-ui DropdownMenu
Swap the antd Dropdown trios (mode picker, base-ref picker, more menu) in
the agent working-sidebar Review pane for the base-ui driven DropdownMenu,
matching the recent task menus / scheduler migration. Also tighten the
sidebar header paddingInline from 16 to 4 to align with the surrounding
density polish.
* 🐛 fix(tasks): replace unsupported onOpenChange with onTitleMouseEnter in context menu
✨ feat(review-panel): hover revert button to discard per-file working-tree changes
Add a hover-revealed Undo icon to each file row in the Review panel's
unstaged view. Clicking opens a Popconfirm; confirming runs a new
`git.revertGitFile` IPC that restores the file from HEAD (or unstages +
deletes when the path doesn't exist at HEAD, covering staged-add and
untracked entries).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Insert pending rows immediately on create folder/document, with
optimistic SWR mutation that rolls back on server error
- Auto-focus rename input on newly created items via onPendingInserted
callback
- Defer rename commits for pending rows until the server create resolves,
then rename against the real row id
- Optimistic recursive delete closes the confirm modal instantly, removes
target + descendants from the tree, and rolls back on failure
- Fix folder path canonicalization in ExplorerTree rename lookup
(toCanonicalTreePath ensures trailing slash for folders)
- Export getItemPathFromEventPath for composed-path–based item resolution
- Add unit tests for toCanonicalTreePath and ExplorerTree event helpers
Add a client-side feature flag override panel that lives behind a
floating button in dev builds. Overrides are persisted to localStorage
and merged into useServerConfigStore.featureFlags so existing flag
consumers see the toggled value without any callsite changes.
The panel is gated by NODE_ENV plus a localStorage opt-in
(LOBE_DEV_FEATURE_FLAG_PANEL_ENABLED = "1"); prod builds tree-shake
the entire feature.
* ✨ feat(builtin-tool-task): expose lobe-task to users and add schedule config
The task tool is now generally available — flip it from a scenario-only
internal tool to a user-toggleable recommended skill, and let the LLM
configure recurring execution (cron or heartbeat) via createTask / editTask.
- Drop `discoverable: false` + `hidden: true` from TaskManifest registration
- Add `lobe-task` to RECOMMENDED_SKILLS so it stays installed by default
- Remove the USER_HIDDEN_BUILTIN_TOOL_IDS allowlist (only contained lobe-task);
update selectors and AgentTool to stop filtering it out
- Extend createTask / createTasks / editTask with `automationMode`,
`schedulePattern`, `scheduleTimezone`, `heartbeatInterval`; editTask also
accepts `maxExecutions`
- Route schedule columns through taskService.update and maxExecutions through
taskService.updateConfig (server merges into tasks.config.schedule);
refresh detail once at the end of editTask
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(builtin-tool-task): split schedule config into dedicated setTaskSchedule tool
editTask was the wrong place for schedule fields — schedule needs its own
verb so the LLM (and any future human-in-the-loop review) can audit cron /
heartbeat changes separately from generic field edits, and createTask should
stay a pure "make a task" verb without automation knobs.
- Drop automationMode / schedulePattern / scheduleTimezone / heartbeatInterval
from createTask + createTasks, and drop them plus maxExecutions from editTask
- Add new `setTaskSchedule(identifier, automationMode?, schedulePattern?,
scheduleTimezone?, heartbeatInterval?, maxExecutions?)` API with its own
manifest entry, executor method, types, i18n key, and inspector
- Schedule columns still route through taskService.update; maxExecutions still
routes through taskService.updateConfig (server merges into
tasks.config.schedule) — same wiring, just moved into the dedicated tool
- Update systemRole to advertise setTaskSchedule + keep editTask description
clean of schedule mentions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(desktop): focus onboarding auth success state
* 🐛 fix(desktop): reset pendingLoginMethod on auth failure/cancel paths
Clear pendingLoginMethod in authorizationFailed, authorizationProgress
cancelled, and remoteServerSyncError handlers to prevent users getting
stuck without a Get Started path when a re-auth attempt fails but a
prior authorization is still valid.
* Delete src/routes/(desktop)/desktop-onboarding/features/LoginStep.test.tsx
---------
Co-authored-by: Innei <inbox@innei.in>
* ♻️ refactor(spa): use __DEV__ define instead of process.env.NODE_ENV
The Vite `__DEV__` define and its global type declaration are already
in place (plugins/vite/sharedRendererConfig.ts, src/types/global.d.ts).
Replace `process.env.NODE_ENV` checks across SPA-only files with the
`__DEV__` boolean so the bundler can statically eliminate dev-only
branches in production builds.
Server-side files (app/, server/, libs/next, libs/trpc, libs/better-auth,
envs, instrumentation) and modules that are also imported by Next.js
SSR pages (e.g. components/Loading/BrandTextLoading) are intentionally
left untouched to avoid runtime `__DEV__ is not defined` errors.
* fix(vitest): define __DEV__ and related constants for test environment
Vitest runs outside the Vite SPA build pipeline, so the __DEV__ define
injected by sharedRendererDefine was not available during tests. This
caused ReferenceError: __DEV__ is not defined in any test file that
transitively imports code using the __DEV__ constant.
Add a block to vitest.config.mts that mirrors the SPA defines:
- __DEV__: true (test is not production)
- __CI__: mirrors process.env.CI
- __ELECTRON__/__MOBILE__: false (not testing platform-specific code)
* fix: replace missed isDevEnv reference with __DEV__ in AgentMockDevtools
* 🐛 fix(utils): cap image binary at 3.75MB so base64 payload stays under Anthropic's 5MB limit
Anthropic enforces the 5MB image cap on the base64-encoded payload, not the
binary file. Base64 inflates by ~4/3, so a 4.7MB binary file becomes 6.27MB
once encoded and trips `messages.*.content.*.image.source.base64: image
exceeds 5 MB maximum`. The previous MAX_IMAGE_BYTES of 5MB matched against
file.size, letting these images through compression untouched.
Lower the threshold to floor(5MB * 3/4) ≈ 3.75MB in both the frontend
canvas compressor and the server-side Sharp fallback so the progressive
shrink loop keeps going until the base64 payload is safely under the cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(utils): tighten image binary cap to 3MB for extra base64 headroom
Drop MAX_IMAGE_BYTES from 3.75MB (exact 5MB-base64 boundary) to a flat 3MB
so the encoded payload lands around 4MB — clear of any per-provider rounding
or jitter at the 5MB hard limit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(portal): allow TodoList to scroll when expanded content exceeds max-height
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks): route 1–N hotkey to the open submenu instead of defaulting to status
The base-ui SubmenuTrigger doesn't propagate antd's `onTitleMouseEnter`, so
the hover ref in the right-click context menu never updated and every number
press fell back to the status submenu. The standalone Priority/Status tag
dropdowns also showed 1–N hints without binding any handler at all.
- Detect the currently open submenu via `data-popup-open` + a per-submenu
`data-task-submenu` marker on the icon; numbers are ignored when no
submenu is open.
- Install a keydown listener on TaskPriorityTag / TaskStatusTag while their
dropdown is open so the hint numbers actually fire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(scheduler): keep Continuous unchanged while editing Max runs
Clearing the Max runs input previously emitted maxExecutions=null, which the
form re-interpreted as Continuous and auto-checked the checkbox mid-edit
(disabling the input before the user could type the replacement number).
Track Continuous as its own state derived from the persisted prop. On clear
we hold the input empty locally without touching Continuous or emitting,
and unrelated emits fall back to the persisted value so they can't flip the
checkbox either.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): always show comment Send button and unify action labels
- Make the Send button visible by default in CommentInput / FeedbackInput
(greyed out when empty) so the field reads as an input instead of vanishing
affordance.
- Align topic action menu labels to Title Case (Stop Run / Open Run /
Copy Topic ID / Copy Operation ID / Copy Link) to match the rest of the
Action microcopy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⚡ perf(scheduler): seed SchedulerForm from props once and own state locally
The previous prop→state useEffects re-synced every time the parent prop
updated, which during the async updateSchedule → refreshTaskDetail roundtrip
clobbered the user's in-flight edits with stale store values — felt awful
on rapid changes.
Drop the three sync useEffects and seed local state from props only at
mount via a lazy useState initializer. The form now owns its values
optimistically; cross-task safety comes from `key={taskId}` on the
parent so the form remounts cleanly when switching tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(scheduler): Notion-style timezone picker — drop underscores, offset on the right
Underscored labels like 'America/New_York (EST/EDT, UTC-5/-4)' read poorly in
the dropdown. Split each option into `label` (underscore → space) and `offset`,
and render the row with the city on the left and a subtle gray offset on the
right, in line with how Notion's timezone picker presents this.
IANA `value` keeps the underscore so cron and Drizzle stay happy. Search now
filters by the human label only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(scheduler): keep zone abbreviations in the timezone offset column
Show 'EST/EDT · UTC−5/−4' instead of just 'UTC−5/−4' so users can recognize
the zone by its common abbreviation alongside the offset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(scheduler): drop awkward ':30' suffix from hourly summary
'Every hour:00' / 'Every 2 hours:30' read like glitched concatenations. Cron
storage always rounds to 0 or 30 minutes, so call out the non-zero case as
'at half past' and stay implicit on the top of the hour.
- Every hour
- Every hour at half past
- Every 2 hours
- Every 2 hours at half past
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(scheduler): collapse advanced settings by default
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⚡ perf(tasks): coalesce post-write refresh and add timezone search
Two follow-up fixes for the AgentTasks scheduler popover.
##### Optimistic schedule writes, single coalesced refresh
Rapid edits in the scheduler form (toggling daily/hourly/weekly, weekday
chips, time, etc.) each triggered `taskService.update` + a full
`internal_refreshTaskDetail` per call. With overlapping requests the
refreshes returned intermediate server state and bounced TaskTriggerTag /
summary text away from the user's latest choice.
- Add `#withCoalescedRefresh` on the task config slice: it tracks a per-task
pending-writes count and only fires `internal_refreshTaskDetail` after the
LAST in-flight write settles.
- Give `updateSchedule` an optimistic `internal_dispatchTaskDetail` so
external readers see the new pattern/timezone/maxExecutions immediately.
- Route both `updateSchedule` and `setAutomationMode` through the coalescer.
##### Timezone picker — search input at the top
The dropdown had antd's implicit type-into-trigger search, which most users
miss. Add a `SearchBar` inside `dropdownRender`, filter the options against
label/value/offset locally, and show an empty state when nothing matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(scheduler): weekday chips only show background when selected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks): dispatch optimistic schedule under nested 'schedule' field
`TaskDetailData` exposes schedule as `schedule.{pattern,timezone,maxExecutions}`,
not flat columns. The previous optimistic dispatch used the DB-style flat keys,
which broke type-check and would never reach the in-memory selectors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): drop Cmd+Backspace shortcut on the Delete menu item
Header dropdown only advertised the hotkey (no handler), and the right-click
context-menu handler is gone too — keeps the visual claim honest and
removes the irreversible-by-keystroke footgun.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(agent-signal): pin `now` in proposal activity tests to fixture window
Two cases relied on the real system clock; once today crossed the
fixture's default `expiresAt` (2026-05-12), pending proposals were
classified as expired and the assertions broke.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): hide '#' placeholder icon for heterogeneous agent topics
Claude Code / Codex topics aren't chat topics in the usual sense, so the
fallback HashIcon in the sidebar row reads as noise. Skip it when the
current agent has a heterogeneousProvider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧪 test(tasks): provide agentMap in TopicItem store mock
`isCurrentAgentHeterogeneous` walks through `currentAgentConfig` which
indexes `s.agentMap[agentId]`. Extend the mocked store state to include
an empty `agentMap` so the selector resolves to `undefined` (= not
heterogeneous) instead of throwing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(cli): remove stale cron entry from generated man page
The cron command was removed from program.ts but the generated man page
still listed it. Regenerated via bun run man:generate.
* 🔖 chore(cli): release 0.0.15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract SIDEBAR_HEADER_ACTION_ICON_SIZE constant for consistent sidebar header ActionIcon sizing
- Pass size prop to ToggleLeftPanelButton
- Simplify Agent selector ActionIcon to use 'small' size preset
- Move layout wrapper styles from Body into TodoList root for better component encapsulation
- Increase Nav gap from 1 to 4 for proper spacing
* ✨ feat: support refreshing recommended task templates
- Add optional `refreshSeed` through `listDailyRecommend` API, service, and
client; SWR key includes it so a refresh actually refetches.
- Frontend stores the seed in sessionStorage (via `useSessionStorageState`)
so a new tab or next day returns to the default daily picks.
- Home Daily Brief shows a "Refresh" affordance on the Recommendations
subtitle row.
- Fix first-card pinning when matched candidates < RECOMMEND_COUNT: fold
the fallback pool in so seed reorders the whole batch instead of locking
position 0 to a single-match template.
Linear: LOBE-8689
* ✨ feat: resolve task-template icon priority
Render the task-template card icon as self > skill provider > interest > Sparkles. Skill icons read required[0] then optional[0], skipping unresolvable providers. URL icons render via @lobehub/ui Image, component icons keep the 28x28 tile.
* ✨ feat: inline skill auth in task template card
Single click "Add task" is now the entire flow: the button stays put, and if a required skill is missing we chain its OAuth popups and create the task automatically. Unauthorized providers (required + optional) appear as compact inline rows above the footer; the provider that already drives the card's main icon is suppressed to avoid duplicating the same logo.
* ✨ feat: add task template detail modal
Open a detail modal when the recommended task template card is clicked,
exposing the full instruction (markdown) plus inline skill auth and the
add-task action. Rename i18n `${id}.prompt` -> `${id}.instruction` to
align with the task table column, and write both `description` and
`instruction` when creating the task. Extract shared `TemplateBriefIcon`,
`useScheduleText`, `useTaskTemplateCreate` and `useVisibleAuthSpecs` so
the card and the modal share the same creation flow and OAuth chaining.
* 🐛 fix: missing Block import in TaskTemplateCard
* ✨ feat: render recommended templates on empty Tasks page
Replace the bare "no tasks" placeholder with a hero landing: greeting,
enlarged inline composer (hero variant), and a 2-column grid of up to
10 recommended task templates. Plumbs a new `count` option through the
service, both routers, the client service, and the recommendations hook
so the home page keeps its 3-card layout while the empty Tasks page
asks for 10.
* 🐛 fix: type cast in resolveTemplateIcon test for unknown interest
* 🌐 i18n: update translations for task template empty-state and other namespaces
* 📝 docs(cloudHeteroContext): add sandbox persistence & gh push rules
Inject ephemeral-sandbox warnings and mandatory GitHub push rules into
the cloud CC context block so every Claude Code run knows:
- The sandbox is wiped after inactivity — local changes will be lost
- All code changes must be committed and pushed before task is complete
- Use gh CLI (pre-authenticated) for GitHub operations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix(cloudHeteroContext): address review comments on sandbox persistence rules
- Remove gh push guidance (gh has no push subcommand; git push is correct)
- Gate gh-auth instructions behind githubToken availability to avoid
auth-dependent commands failing in no-token sandbox runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 📝 docs(cloudHeteroContext): add git push auth fallback guidance
Tell CC that the sandbox has git credentials ready, but if git push
fails it can self-recover via:
1. gh auth setup-git (reconfigures git credential helper)
2. inline token URL as last resort (oauth2:$GITHUB_TOKEN@github.com)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔨 chore: control skill triggering via frontmatter flags
- Rename debug skill to debug-package (avoid confusion with debugging workflows)
- Add disable-model-invocation to add-* skills so they are manual-only
- Add user-invocable: false to reference/architecture skills so they auto-load only when relevant
* 🔨 chore: rename skill reference dirs to plural references
Align with the skill-creator convention (scripts/, references/, assets/).
* 📝 docs(skills): split oversized SKILL.md files and refine triggers
- upstash-workflow: 1126L → 189L, extract implementation / best-practices / examples references
- data-fetching: 854L → 613L, move parent-keyed-map walkthrough to references
- store-data-structures: 625L → 314L, extract types and reducer references
- upstash-workflow/cloud.md, version-release/release-notes-style.md: add TOCs
- linear: rewrite ALL-CAPS MUSTs into prose explaining why; mark user-invocable: false
- version-release: mark disable-model-invocation: true (manual /version-release only)
- debug-package: expand description with concrete trigger phrases and tokens
* 📝 docs(skills): regularize microcopy structure
Move language-specific guidelines into references/zh.md and references/en.md
so SKILL.md can point to them via the standard progressive-disclosure pattern.
Previously the two files sat next to SKILL.md but were not referenced anywhere,
making them invisible to Claude Code loading.
* 📝 docs(skills): move builtin-tool refs into references subdir
Aligns builtin-tool with the references/ layout used elsewhere
(microcopy, store-data-structures). 3 md files move, SKILL.md
links updated.
* 📝 docs(skills): broaden trigger descriptions for core skills
Adds concrete API names, file paths and natural-language phrases so
auto-triggering catches more relevant prompts. Touches zustand,
drizzle, i18n, react, typescript, modal, hotkey.
* 📝 docs(skills): add argument-hint to user-only skills
Previously, clicking the clear button on HotkeyInput triggered both
`onClear` and `onChange` (since HotkeyInput internally calls
`setHotkeyValue('')` which fires `onChange`). This caused two
concurrent requests to `updateDesktopHotkey` and showed two toast
messages (success/error) for a single user action.
Fix: remove the redundant `onClear` prop. HotkeyInput's clear action
already fires `onChange('')`, so the single `onChange` handler is
sufficient.
Co-authored-by: Innei <i@innei.in>
* ♻️ refactor(web-onboarding): merge agent-marketplace identifier into onboarding tool
Drop the standalone `lobe-agent-marketplace` builtin tool and fold its
`showAgentMarketplace` / `submitAgentPick` APIs into `lobe-web-onboarding`
so onboarding exposes a single tool identifier.
- Move marketplace API entries (with humanIntervention/renderDisplayControl)
into WebOnboardingManifest; extend WebOnboardingApiName.
- Compose AgentMarketplaceExecutionRuntime inside WebOnboardingExecutionRuntime;
the client WebOnboardingExecutor now owns showAgentMarketplace/submitAgentPick
with telemetry hooks. Drop the separate client/server executor + runtime files.
- Merge marketplace Inspector / Intervention / Render maps under the
web-onboarding identifier. Remove AgentMarketplace* entries from
builtin-tools registries and from the builtin web-onboarding agent's
plugins list.
- Switch customInteractionHandlers to route by (identifier, apiName) so
the marketplace picker handler fires only on `showAgentMarketplace`.
- Drop the `lobe-agent-marketplace` fallback string in
OnboardingActionHintInjector; match by apiName only.
- Rename plugin/setting locale keys under `lobe-web-onboarding.*`.
* 🐛 fix(onboarding): reserve scroll headroom for agent marketplace overlay
- Add a footerSlot spacer in ChatList matching the marketplace panel height so the latest message can be scrolled into view above the absolute overlay.
- Nudge the marketplace overlay inset by 2px to hide subpixel border seams.
- Document turn output order in the onboarding system role to avoid trailing filler text after tool calls.
✨ feat(builtin-tool-web-onboarding): add Render for saveUserQuestion + showAgentMarketplace
Tool messages for `saveUserQuestion` and `showAgentMarketplace` previously
fell back to the raw Arguments/Response table once the call resolved
because neither API had a Render registered. Wire both up:
- `saveUserQuestion`: new Render mirroring the Intervention's detail-card
style — agent identity (emoji + name), full name, and interests chips —
rendered conditionally per the fields actually saved.
- `showAgentMarketplace`: reuse the existing `SubmitAgentPick` Render.
After the picker submits, `customInteractionHandlers` rewrites the
`showAgentMarketplace` tool message's `pluginState` to the same
`{ summaries, installedAgentIds, ... }` shape, so the card grid
renders without a new component.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(knowledge-base): share runtime across client/server via KnowledgeBaseSearchService
Extract a server-side `KnowledgeBaseSearchService` (semanticSearchForChat
fan-out + getFileContents branching + groupAndRankFiles) so both the lambda
chunk router and the builtin tool server runtime orchestrate RAG through one
implementation. Wire the builtin knowledge-base tool to the shared
ExecutionRuntime in the package by moving the client executor to
`src/client/executor/` and registering a thin server runtime factory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(knowledge-base): move PG 23505 handling into adapters, restore executor path
ExecutionRuntime is dual-end so it cannot detect PG error codes — only the
server adapter can. Move the unique-constraint check there and translate the
lambda router's `FILE_ALREADY_IN_KNOWLEDGE_BASE` sentinel in the client
adapter, so the runtime's generic catch surfaces the human-readable message
on both code paths. Restore `src/executor/` as a top-level sibling of
`src/client/` to match the convention of every other builtin tool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(knowledge-base): collapse executor into /client, drop ./executor export
The executor is just another client-only adapter (alongside Inspector and
Render) — no reason for it to sit at the package root with a dedicated
subpath. Move it under `src/client/executor/`, re-export from
`src/client/index.ts`, drop the `./executor` entry from package.json, and
update the consumer to import from `@lobechat/builtin-tool-knowledge-base/client`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(knowledge-base): cover KnowledgeBaseSearchService
13 unit tests across both methods:
- getFileContents: docs_* direct read, missing doc, file_* via findByFileId,
parseFile fallback, parse failure surfaces as error entry, missing file,
mixed batch.
- semanticSearchForChat: chunk grouping + relevance ranking, BM25 skip when
no knowledgeIds, knowledgeIds → fileIds expansion, vector/BM25 isolated
failure capture (preserves the other path's results + structured
rejections), full failure path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(aiAgent): introduce deviceToolRegistry as single source of truth
Centralise "what counts as a device tool" into one module so the next
device-tool addition only touches one file. Removes the hardcoded
`new Set(['local-system', 'remote-device'])` from `deviceToolAudit.ts`,
which had drifted from `LocalSystemManifest.identifier` /
`RemoteDeviceManifest.identifier` imports elsewhere.
Foundation for the LOBE-8768 activator-bypass fix landing next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(aiAgent): block activator from bypassing canUseDevice gate
External bot senders could still reach the owner's machine by having the
LLM call `lobe-activator.activateTools(["lobe-remote-device"])`, because
`enableCheckerFactory.allowExplicitActivation` short-circuits before the
canUseDevice rule, and the engine's `manifestSchemas` always contained
the full builtin list (LOBE-8768 B1).
Fix by filtering builtin manifests **physically** through
`buildAllowedBuiltinTools` at both feed-points (ToolsEngine input and
the activator-discovery `toolManifestMap`). When `canUseDevice=false`,
the device manifests no longer exist in either map, so explicit
activation cannot resolve them — the rule-layer gate becomes
defense-in-depth instead of the sole barrier.
Validates with the prod incident's repro path: an external sender's
`<available_tools>` no longer advertises `lobe-remote-device`, and an
activator call to enable it returns "not found".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(bot,messenger): centralise isOwner derivation in buildBotContext
The same fail-closed expression
`!!operatorUserId && senderExternalUserId === operatorUserId` was
duplicated across `BotMessageRouter.onNewMention`, `.onSubscribedMessage`,
the DM catch-all, and `MessengerRouter.dispatchToAgent` — four sites,
one rule, one place to silently regress.
Route all four through `buildBotContext`. The helper now owns the
fail-closed contract referenced by `ChatTopicBotContext.isOwner`'s
docstring, so adding the next platform/router can't accidentally
default to "trusted when in doubt".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(aiAgent): apply device filter post-merge across all manifest sources
The previous fix only filtered the `builtinTools` source. An installed
plugin or a Skill/Klavis manifest declaring
`identifier: 'lobe-remote-device'` would still survive in
`manifestSchemas` and reach `toolManifestMap` via either
`getEnabledPluginManifests` or the direct ingest loops in
`aiAgent/index.ts` — letting an external bot sender activate the device
identifier through the activator.
Two changes close the gap:
1. `ServerAgentToolsEngineConfig.excludeIdentifiers` — applied **after**
combining plugin + builtin + additional manifests in
`createServerToolsEngine`. `createServerAgentToolsEngine` passes
`DEVICE_TOOL_IDENTIFIERS` whenever `canUseDevice` is false.
2. `isManifestIngestAllowed` in `aiAgent.execAgent` — a single
identifier guard reused at every `toolManifestMap` / `toolSourceMap`
write (engine-returned plugin manifests, lobehub-skill loop,
klavis loop). New ingest points inherit the wall automatically.
New test pins the regression: a plugin + an additional manifest
spoofing the device identifiers are dropped from `availablePlugins`
when `excludeIdentifiers` is set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): snapshot agent model into task.config at create time
Pin the assignee agent's current model/provider into task.config when a
task is created so later changes to the agent's default model don't
silently affect already-created tasks. On first run, backfill the
snapshot for tasks created before this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task-runner): fall back to inbox agent when task has no assignee
`TaskRunnerService.runTask` previously threw `BAD_REQUEST` for any task
without `assigneeAgentId`, which broke runs created without `--agent`.
Resolve and persist the user's built-in inbox agent instead, surfacing
an `INTERNAL_SERVER_ERROR` only if that resolution itself fails.
Picked from #14671 (closes once landed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task): collapse router orchestration into TaskService
Move multi-step task verbs out of the TRPC router into `TaskService`:
`createTask`, `cancelTopic`, `deleteTopic`, `runReview`, `updateStatus`,
`previewSubtaskLayers`, `runReadySubtasks`. The router keeps only input
validation + error wrapping; the tool runtime now shares the same
`createTask` path (was duplicating the model snapshot + parent
resolution).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🚨 ci: fix tsgo errors from TaskService extraction
`runReadySubtasks` router was rebuilding the `data` payload via a
conditional spread, which forced TS to infer a discriminated union that
broke `result.data.skipped` access in the integration test. Pass the
service result straight through so `skipped` stays a single optional
field. Also cast the stubbed `taskService` in the tool runtime unit
tests to bypass strict structural typing — same pattern the other
dep stubs already use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 chore: drop task template tracking
The recommendation surface is about to be redesigned, so the analytics
funnel added in #14517 is being removed up front. A fresh tracking
schema will land alongside the redesigned UI.
- Delete `analytics.ts` plus its test and the tracking-focused
`TaskTemplateCard.test.tsx`.
- Drop `RecommendedTaskTemplate` / `TaskTemplateRecommendationSource` /
`TaskTemplateFallbackPool` and revert the service to plain
`TaskTemplate[]`.
- Strip impression, dismiss, create-clicked/result and
skill-connect-clicked/result calls from `TaskTemplateCard.tsx`, while
keeping the createTask + navigate-to-task flow from #14540.
- Remove `recommendationBatchId` / `userInterestCount` / `onCreated`
plumbing from `useDailyBriefRecommendationsUI`,
`DailyBriefRecommendationsView`, and the card props.
- Revert `useSkillConnection` to the pre-tracking variant (no
onConnectResult / SkillConnectionResult).
* 🐛 fix: remove created template from recommendation cache
After #14540 changed the create-task flow to auto-navigate to
`/task/{id}`, removing the `onCreated` plumbing from #14517 in the same
sweep meant the SWR recommendation cache was never mutated on success.
Combined with the server-side `recordCreated` being a no-op and
`listDailyRecommend` not excluding created IDs, returning to Home
showed the same recommendation as actionable again — letting users
trigger duplicate scheduled tasks from the same template.
Re-add the minimal cache-eviction plumbing (no analytics):
- TaskTemplateCard exposes `onCreated` and calls it on success
- useDailyBriefRecommendationsUI shares `removeTemplateFromList` for
both dismiss and created flows
- DailyBriefRecommendationsView passes `onCreated` through
* 🐛 fix: drop unreachable aihubmix empty-apiKey test
The `should return empty array when API key is missing` test asserts a
contract that doesn't hold: RouterRuntime.models() constructs the
underlying runtime via the OpenAI-compatible factory before calling
modelsOption, and the factory throws InvalidProviderAPIKey on empty
apiKey at construction time — so aihubmix's own `if (!apiKey) return []`
short-circuit can never actually fire.
Just delete the dead test. The defensive guard in aihubmix's modelsOption
stays as intent documentation. Also tighten an implicit-any in the
adjacent `should normalize model_id field to id` test.
* 🔥 chore: drop dead empty-apiKey guard in aihubmix modelsOption
* 💄 style: tighten aihubmix apiKey assertion to string
* 💄 style: increase chat topic title length
- bump initial topic title slice from 20 to 40 chars
- bump dev fallback slice from 30 to 40 chars
- bump thread title slice from 20 to 40 chars
- raise LLM summary title prompt limit from 50/10w to 80/15w
* 💄 style: bump topic/thread title slice from 40 to 80 chars
Align slice limits with the LLM summary prompt cap (80 chars) so the
initial visible title is no shorter than what the summarizer can return.
* fix(aihubmix): use full models endpoint to return complete model list
The /v1/models endpoint at api.aihubmix.com returns only per-user-group
models (~256). The new endpoint at aihubmix.com/api/v1/models returns
the complete catalog (800+). Fetch from the full endpoint directly.
* fix(aihubmix): normalize model_id to id from full models endpoint
The https://aihubmix.com/api/v1/models endpoint uses `model_id` instead
of `id`. Map it to `id` before passing to processMultiProviderModelList
to prevent toLowerCase() errors and empty model list.
* fix(aihubmix): add apiKey guard, AbortController timeout, and better error messages
- Extract apiKey with runtime guard to fail fast when key is missing
- Add AbortController with 10s timeout to prevent indefinite hanging
- Include response body in error message for easier debugging
- Add APP-Code header comment pointing to docs
- Expand tests: mock global fetch, cover missing key / HTTP error / network error / AbortError cases
* fix(aihubmix): add field mapping adapter and fix timeout scope
Address review feedback from #14511:
- Update AiHubMixModelCard interface to reflect the new endpoint schema
with full JSDoc (model_id, desc, types, features, input_modalities,
context_length, max_output, pricing.cache_read/cache_write)
- Add mapAiHubMixModel() to adapt API response fields to LobeHub model
card fields before passing to processMultiProviderModelList:
desc -> description
model_name -> displayName
context_length -> contextWindowTokens
max_output -> maxOutput
types -> type (llm/t2t->chat, image_generation/t2i->image,
video/t2v->video, tts, stt, embedding,
rerank/reranking->rerank)
pricing.cache_read -> pricing.cachedInput
pricing.cache_write -> pricing.writeCacheInput
features(tools/function_calling) -> functionCall
features(thinking) -> reasoning
features(web) -> search
input_modalities(image) -> vision
- Fix timeout scope: move clearTimeout into the finally block so the
AbortController stays active during response.json() body read, not
just during the initial fetch() call
- Update baseURL from https://api.aihubmix.com to https://aihubmix.com
to match official integration docs (https://docs.aihubmix.com/cn/api/Aihubmix-Integration)
- Strengthen normalize test: assert list.some(m => m.id === 'some-model')
instead of just Array.isArray to detect normalization failures
- Add field-mapping test using vi.spyOn on processMultiProviderModelList
to assert that all adapted fields are passed correctly
* fix(aihubmix): filter out unsupported rerank types to prevent chat fallback
- Remove rerank/reranking from TYPE_MAP; they have no LobeHub AiModelType
equivalent and would silently fall back to 'chat' in processModelCard
- Add UNSUPPORTED_AIHUBMIX_TYPES set and filter before mapAiHubMixModel()
- Add regression test asserting rerank/reranking models are excluded and
llm models still pass through
---------
Co-authored-by: Bianzinan <bianzinan@users.noreply.github.com>
* 🐛 fix(onboarding): skip marketplace on early exit, drop CJK examples in prompts
Honor the user's wish to leave: when the onboarding agent detects a true
early-exit signal in any phase, persist what is known, send a brief
farewell, and call finishOnboarding directly. The marketplace handoff is
mandatory only on normal Phase 4 / Summary completion. Previously the
spec forced the agent to invent categoryHints from environment cues
when discovery was thin, producing noisy recommendations for users who
explicitly asked to stop.
- Replace systemRole §Early Exit with a 4-step flow (no marketplace, no
summary), and remove the trailing "respect their time" rationale that
contradicted the new policy.
- Update toolSystemRole turn-protocol exception accordingly; mark
persistence as best-effort (do not retry on failure) since the
Pre-Finish Checklist is overridden on early exit.
- Update OnboardingActionHintInjector L101/L127 hints to match the new
flow, and append an EXCEPTION clause to the Summary not-opened hint
so a true exit signal in Summary skips the marketplace too.
- Strip CJK example phrases from prompt text; rely on the LLM's
multilingual recognition with "equivalents in any language" hints.
* 🔨 refactor(FollowUpChips): remove unused consume function and reset editor state on chip click
🔨 style(InterventionBar): remove overflow hidden from container style
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(ci): align FollowUpChips test with removed consume and increase timeout for PGlite cold-start
---------
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(hetero-agent): read-only SubAgent threads with breadcrumb header and thread switcher
- Hide chat input on SubAgent threads (execution is driven by the parent agent) and replace it with an inline read-only hint
- Render the hint as the last item inside the virtual list so it scrolls with messages instead of being pinned to the viewport bottom
- ChatList exposes a new `footerSlot` prop that VirtualizedList injects as a synthetic trailing data item
- Header now shows `topic / thread` breadcrumb; thread title is a popover trigger that lists sibling threads in the same topic for one-click switching
- Hide the working-directory tag while inside a thread — directory switching doesn't belong in this read-only view
- Unify user-facing strings to "SubAgent" (badge, hint, open/close labels)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(chat-input): soften queue tray preview borders
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): scrollToBottom lands on the true last VList item
scrollToBottom targeted displayMessages.length - 1, which leaves any
trailing synthetic items (spacer, SubAgent footer hint) below the
viewport. In SubAgent threads this kept atBottom = false after the
BackBottom click or auto-scroll, so the button appeared stuck.
VirtuaScrollMethods now exposes getTotalCount, which VirtualizedList
fills from the live data length (messages + spacer + optional
footerSlot) via a ref. scrollToBottom uses that to scroll to the real
last index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(chat-input): show skeleton in action bar while config is loading
Before agent / group config hydrates, action buttons read DEFAULT_*
fallbacks and the send button would dispatch against a not-yet-ready
target. Add an `isConfigLoading` prop on DesktopChatInput that swaps the
action bar + send area for skeleton placeholders. The chat page passes
`agentSelectors.isAgentConfigLoading`, group chat passes
`agentGroupSelectors.isGroupsInit`. The editor itself stays usable so
users can start typing immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(home,i18n): use 已阅 for brief confirm/confirmDone in zh-CN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(home): use 确认完成 for brief.action.confirmDone in zh-CN
confirmDone signals the terminal transition (task marked complete),
not just dismissing the brief, so 已阅 loses the semantic distinction
from `confirm`. Use 确认完成 to match the EN intent ("Confirm complete").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(home): use "Confirm complete" for brief.action.confirmDone in en-US
Match the semantic distinction the call site relies on:
`confirm` is dismiss-only for recurring scheduled runs, while
`confirmDone` marks the terminal completion transition. The test
mock already used "Confirm complete" — align the source defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(home): add Recommendations module with hetero agent action library
Introduce a `Recommendations` section that renders above the existing daily-brief
task templates. The module is driven by an extensible action registry with per-action
eligibility checks; the first registered actions surface "Add Claude Code agent" and
"Add Codex agent" cards on desktop when the matching local CLI is detected and the
user hasn't added that hetero agent yet.
- New `src/features/Recommendations/` with action types, registry, hetero-agent
factory, eligibility hook, parallel CLI detection (SWR-cached) and card UI.
- Extract `createHeterogeneousAgent` from `useCreateMenuItems` into a shared
`useCreateHeteroAgent` hook so the sidebar menu and Recommendations card share
one creation path (create + refresh sidebar + navigate to chat).
- `DailyBrief` now renders `<Recommendations />` in place of the standalone
template-only section; visibility is driven by the new
`useRecommendationsVisible` hook.
- Add `recommendations.*` i18n keys to the `home` namespace (default + zh-CN +
en-US dev preview).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(home): polish Recommendations card with brand avatar and tighter copy
Use brand Avatar icons with rounded square shape, drop the duplicate title, and tighten copy (Coding Agent tag, Add Agent CTA).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2)
Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's
built-in tool short-circuits in `-p` mode, so we host an in-process MCP
server that exposes an equivalent `ask_user_question` tool. The handler
blocks until the consumer submits an answer (or the 5min deadline / op
shutdown fires), surfacing a structured `agent_intervention_request` /
`agent_intervention_response` round-trip on the existing event stream.
Added in this commit:
- `packages/heterogeneous-agents/src/askUser/`
- `AskUserBridge` — per-op pending map with timeout / cancel / progress
keepalive support; emits an async-iterable of outbound events
- `AskUserMcpServer` — process-wide HTTP/Streamable MCP server,
`?op=<id>` query routes via `AsyncLocalStorage` →
`onsessioninitialized` → sessionId↔opId map; tool handler hands off
to the matching bridge and pumps `notifications/progress` back to CC
every 30s as wire-level keepalive (required for >5min waits, see
spike notes)
- `constants.ts` — shared tool/server names + the stable `apiName`
the adapter rewrites to
- Unit tests cover bridge lifecycle (resolve / cancel / timeout /
progress / event stream) and an end-to-end MCP probe via
`StreamableHTTPClientTransport`
- `packages/agent-gateway-client/src/types.ts` — wire-level
`agent_intervention_request` / `agent_intervention_response` event
variants + payload interfaces. Re-exported through the package barrel.
- `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's
`tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter
rewrites `apiName` to `askUserQuestion` so the renderer routes on a
clean domain key. Identifier stays `claude-code`. Applied to both the
main-agent and subagent paths for symmetry (subagent ask isn't
expected today, but doesn't hurt).
- `src/server/routers/lambda/aiAgent.ts` — Zod input schema for
`aiAgent.heteroIngest` extended with the two new event types so the
CLI sandbox can forward them through the server.
No producer wiring yet — Steps 3-5 plug this into Electron main, the
renderer executor, and the new UI.
* ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3)
Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the
desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now
goes live during real prompts; renderer-submitted answers route back via
new IPC.
Changes
- `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add
optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so
controller-managed temp configs flow into the driver.
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`
— append `--mcp-config <path>` when provided. Disallowed-tools pin
stays so CC's built-in AskUserQuestion remains off (avoids double-
registration of the same tool name).
- `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts`
- Lazy-singleton `AskUserMcpServer` started on first claude-code prompt
(de-duped concurrent first-callers via in-flight promise).
- Per-op `setupInterventionForOp(opId, sessionId)`: registers an
`AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with
`alwaysLoad: true` so CC eager-loads the tool (1-hop call, no
ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()`
into the existing `heteroAgentEvent` broadcast.
- Cleanup paths: exit handler `await intervention.cleanup()` settles
pending MCP handlers + unlinks the temp config; pre-spawn errors
short-circuit the same cleanup so we don't leak bridges on
`buildSpawnPlan` / trace-session failures.
- `before-quit` stops the MCP server (in addition to killing CC
processes).
- New `@IpcMethod() submitIntervention({ operationId, toolCallId,
result?, cancelled?, cancelReason? })` — renderer side will dispatch
answers / cancellations through this in Step 4/5.
- codex unchanged — bridge setup is gated on `agentType === 'claude-code'`.
- `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy
for `submitIntervention`.
- New `claudeCode.test.ts` covers the four driver-arg paths
(`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay
disallowed). Existing 28 controller tests still pass.
What still doesn't run end-to-end
- The renderer `heteroExecutor` doesn't consume `agent_intervention_request`
yet — events go through the broadcast but the chat store ignores them.
- No UI to render the intervention card or to call `submitIntervention`.
Both lands in Steps 4/5 next.
* ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4)
Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId`
from MCP `_meta`) instead of a random UUID, so the
`agent_intervention_request` event references the same id as the existing
tool message on the renderer side.
Renderer-side `heteroExecutor` learns the new event:
- Added `persistInterventionRequest(...)` next to `persistToolResult` —
stamps `pluginState.askUserQuestion` (apiName + identifier + questions
parsed from `arguments` + deadline + status='pending' + toolCallId)
onto the matching tool message via `messageService.updateToolMessage`.
- New branch in `handleStreamEvent` for `'agent_intervention_request'`:
defers behind `persistQueue` (so it lands AFTER `persistToolBatch`
populates `toolMsgIdByCallId`), then mirrors the same pluginState onto
the in-memory message via `internal_dispatchMessage` so the UI lights
up immediately — no fetchAndReplaceMessages round-trip needed.
- The eventual `tool_result` for the same toolCallId hits the existing
`tool_result` branch unchanged: it overwrites `pluginState` with
whatever the result carries (typically undefined for our MCP tool, so
`pluginState.askUserQuestion` clears and the intervention UI yields to
the regular Render).
Bridge tests cover the new contract:
- caller-supplied toolCallId becomes the wire correlation key
- duplicate-toolCallId pendings reject loudly so two-handler clobbers
surface immediately
153 package tests + 1167 desktop main tests + 51 hetero executor tests
still green; type-check clean.
* ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5)
Dedicated Render for the synthetic `askUserQuestion` apiName the adapter
rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives
under CC's render registry so the existing chat tool-detail flow picks
it up automatically — no changes to the conversation framework.
- New `AskUserQuestionItem` / `AskUserQuestionArgs` /
`AskUserQuestionPluginState` types (mirrors CC's own
AskUserQuestion schema verbatim).
- `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'`
member so the renders / inspectors / streamings registries can key
off the same enum value.
- `client/Render/AskUserQuestion/index.tsx` is the component:
- `pluginState.askUserQuestion?.status === 'pending'` → renders the
questions form (Select for single-select, CheckboxGroup for
multi-select), a 5-min countdown ticking once a second, Submit /
Skip buttons. Reads `operationId` via `messageOperationMap` so we
can route through `heterogeneousAgentService.submitIntervention`.
- Otherwise → renders the questions as muted captions plus the
final answer text from `content`. Surfaces a warning when the
tool_result was an error (timeout / cancelled / session ended).
- Submit button stays disabled until every question has a
selection; Skip always enabled (sends `cancelled: true`).
- `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers
the new component.
What this does NOT do
- Doesn't touch `BuiltinToolInterventions` — the form is rendered
inside the regular tool body (Render slot), not the canonical
intervention slot. Cleanest for now: the framework intervention
flow assumes `submitToolInteraction` store actions, which would
fight our IPC path. We can refactor onto that surface later if
CC grows additional interactions (approval, file picker).
- Doesn't translate strings — i18n in a follow-up.
Type-check clean. Step 6 (real desktop e2e via CC) is next.
* ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up)
Step 5 registered the Render component but stopped at the registry — the
chat tool-detail still returned the loading placeholder while
`isToolCalling` was true, so users only ever saw a spinner during the 5
min intervention window.
Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on
CC + apiName=askUserQuestion tool messages) and route to the registered
builtin Render inline before the placeholder branch. Once the
intervention resolves, the eventual `tool_result` clears
`pluginState.askUserQuestion` and the regular Render takes over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up)
LOBE-8519 left two TODOs in `generationSlice` where hetero runtime
silently fell through to client mode — regenerate would secretly hit the
agent's underlying LLM, and continue would synthesize a fake "please
continue" turn that confuses CC / Codex.
- regenerateMessage: re-create the assistant row branched off the same
user message, resolve resume sessionId (drop on cwd mismatch), then
spawn a child `execHeterogeneousAgent` op so Stop only kills the
executor, not the parent regenerate op. Mirrors sendMessage's hetero
branch.
- continueGenerationMessage: hetero CLIs have no continue primitive —
each prompt is a fresh user turn — so bail out instead of polluting
the session.
- continueGenerationMessage: gateway mode now branches a server-side
resume run instead of falling through to client.
Surfaced while testing CC AskUserQuestion end-to-end on the
LOBE-8725 branch (regenerating after an answered question went through
the wrong runtime).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2
Two bugs surfaced when invoking the local-testing helper from a fresh
session on macOS:
- `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit
code propagates through `pipefail`. With `set -e`, an empty pid set
silently kills the whole script — `do_start` reported success, no
Electron, no error. Trail with `|| true`.
- `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`;
process-tree teardown still works because `expand_descendants` walks
the tree directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725)
`AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across
every CC subprocess. The SDK transport latches `_initialized=true`
after the first `initialize`, so the second op's CC subprocess sees
`Invalid Request: Server already initialized` (400) and reports the
`lobe_cc` server as `failed`. From the model's POV the MCP tool is
absent — it falls back to ToolSearch, can't find anything, and
verbalizes the question instead.
Refactor to the canonical multi-tenant pattern: one transport + one
`McpServer` per session, looked up by the SDK-managed `mcp-session-id`
header. New transports are minted on the first POST without a session
id (must be an `initialize` request); subsequent requests route via
the stored map; `onsessionclosed` cleans up.
The first run of any process still works as before — this only matters
once a second op spins up. Added a 3-op sequential regression test
that fails on the old single-transport implementation and passes now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725)
Step 5's first cut shoehorned the pending form into the Render slot and
drove submit/skip with a custom `pluginState.askUserQuestion.status`
field, which forced three layers of glue:
- `Tool/Detail` had to bypass the loading placeholder via an
identifier+apiName hardcode so the form would surface during
`isToolCalling`
- The executor had to `messageService.getMessages → replaceMessages`
after `agent_intervention_request` to drag the freshly-created tool
row into in-memory state (the framework's own `tool_end →
fetchAndReplaceMessages` only fires after the user answers)
- The executor also had to `associateMessageWithOperation` for the tool
row so the form could look up the running CC op for IPC
All three were patches around skipping the canonical surface. This
commit moves AskUserQuestion onto `pluginIntervention.status='pending'`
and the `BuiltinToolInterventions` registry, which the framework
already drives end-to-end:
- `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx`
— pure form, no IPC, no store reads. Resolves through the standard
`onInteractionAction({type:'submit'|'skip'|'cancel'})` callback.
- `Render/AskUserQuestion` shrinks to the answered/aborted view only;
the framework hides Render while pending, so no status switching.
- New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}"
chip in the inline tool body, matching the rest of CC's tools.
- Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new
`ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`;
`BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry.
Hetero needs a different action handler than `submitToolInteraction`
(which spawns `executeClientAgent` — wrong for a CC subprocess that's
already blocked on an MCP call). Two thin pieces wire that:
- `submitHeteroIntervention` (chat store) — sets
`pluginIntervention` via `optimisticUpdateMessagePlugin` (which
already syncs DB + in-memory + parent-assistant `tools[].intervention`
in one shot), then forwards the answer through
`heterogeneousAgentService.submitIntervention` IPC. Operation lookup
walks the tool message's `parentId` to hit the assistant's
`messageOperationMap` entry — drops the explicit
`associateMessageWithOperation` call from the executor.
- `customInteractionHandlers.isHeteroInteractionIdentifier` flags
`ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits
there before reaching the existing `submitToolInteraction` path.
Executor change collapses to one line:
`optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`.
The post-intervention refresh, the associate call, and the
`persistInterventionRequest` helper all go away.
Removed:
- `AskUserQuestionPluginState` type (custom field is gone)
- `Tool/Detail` `askUserPending` inline-render branch
- Executor `messageService.getMessages + replaceMessages` round-trip
- Executor `associateMessageWithOperation` for tool rows
- `persistInterventionRequest` helper
Verified end-to-end against a real CC subprocess on desktop:
- Inline body shows the new Inspector chip; pending form lives in the
bottom InterventionBar (canonical surface)
- Submit ships answer through MCP, CC continues with structured result
- Skip flips status to `rejected`, framework's RejectedResponse
shows "User skipped"; CC receives isError and falls back to text
- `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op
(the per-session transport fix from the previous commit)
- `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725)
Select dropdown was the wrong primitive — it hides options behind an extra
click and doesn't read like a question to answer. CC's underlying tool is
1-4 questions × 2-4 options, so the whole option set always fits inline.
- Each option renders as a clickable card: numbered chip (1/2/3/4) +
bold label + secondary description on a single row. Hover tints the
background; selected state lights up `colorPrimary` on both the chip
and the card outline so the pick is unmistakable at a glance.
- Multi-select (`q.multiSelect`) toggles instead of replacing, with a
"(multi-select)" hint in the question header.
- Multi-question support gets a proper visual hierarchy: each question
past the first sits below a dashed divider, headed by a `Q1/N` tag
+ the original `q.header` chip. The `Q*/N` lets the user track
progress without counting.
- Inspector picks up the question count too: now shows
"askUserQuestion · {first header} +N" when multiple are queued.
Verified end-to-end on desktop with a CC-driven 2-question prompt
(4-option + 3-option). Both selections feed back to CC as a single
"User answers" payload, CC echoes both picks in its continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725)
- Multi-question forms now use a top tab strip; single question renders inline.
- Picking a single-select option auto-advances to the next unanswered question.
- Drafts persist to tool message `pluginState.askUserDraft` so picks survive
remount / HMR; new `setInterventionDraft` action on the chat store dispatches
the pluginState patch.
- Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for
every unanswered question instead of letting the bridge time out into a
cancelled isError — model gets a structured answer it can act on.
- Visual: selected option now uses filled `colorPrimaryBg` + right-aligned
check icon; index chip stays neutral.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725)
The async exit-handler cleanup raced Electron's main-process teardown and
left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync
unlink in the quit hook is the only reliable guarantee.
Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q
or `app.quit()`, not on external kills (test harness, OS shutdown).
Verified by manual test: pending askUserQuestion forms now leave zero
residue after both Cmd+Q and SIGTERM paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725)
Submit now writes the structured `{ questionText: pickedLabel(s) }` payload
to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so
Render no longer has to scrape the bridge's prose `User answers:` content.
Render shows one Q&A block per question — header + question + a checkmark
card per picked option (multi-select fans out into multiple rows). Falls
back to a `—` placeholder when answers are missing (older messages or
skipped flows), and keeps the existing `pluginError` warning for cancel /
no-answer paths.
Also surfaces the answers in the Skill state inspector tab, which was
previously empty for completed askUserQuestion messages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725)
Locks down the regression fixed in c0de0cdb7c — async exit-handler cleanup
losing to Electron's main-process teardown. Four cases: `before-quit`
(Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown),
`SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not
throw on the second pass).
`process.on` and `process.exit` are stubbed in the signal-path tests so the
controller's listener attaches to a spy, not the test runner's process —
otherwise we'd leak a real SIGTERM listener every test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(copyable-label): wrap long values instead of truncating
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(copyable-label): make wrap an opt-in via Descriptions prop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(descriptions): omit GridProps wrap to avoid type collision
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(model-runtime): enrich stream parse errors with provider/model context
When the OpenAI / Anthropic SDK iterator throws (most often a JSON
SyntaxError on a malformed SSE chunk — e.g. an upstream response with an
illegal backslash escape), `convertIterableToStream` previously only
surfaced `message`/`name`/`stack`. Downstream error logs (agent-gateway
errors table) end up with just "Bad escaped character in JSON at
position 160050" and no way to correlate which provider/model produced
it or whether the same offset keeps recurring.
This change threads optional `{ provider, model }` context through
`convertIterableToStream` / `readableFromAsyncIterable` and enriches the
FIRST_CHUNK_ERROR payload with:
- `provider` / `model` so triage can group identical upstream failures
- `parsePosition` extracted from V8 JSON SyntaxError messages
- `causeName` / `causeMessage` when `error.cause` is set (many wrapped
errors carry the actionable detail in `cause` and the bare triplet
drops it)
Threaded through OpenAI/Responses/Anthropic stream handlers, which all
already receive `payload` containing provider/model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): walk error.cause for parsePosition + JSON-safe payload
Two review findings on #14636:
1. Wrapped SyntaxErrors lost their parsePosition. Provider SDKs commonly
rethrow `JSON.parse` failures wrapped in their own error class
(e.g. `APIError(cause: SyntaxError)`), so the outer `error.name` is
no longer `'SyntaxError'` and the previous check skipped extraction
for the exact case this enrichment was meant to diagnose. Now
`extractParsePosition` walks both the outer error and any `Error`
cause, and accepts any error whose message still carries the
`"JSON at position N"` signature even if the SyntaxError name was
lost in wrapping.
2. Cause cloning could blow up the entire diagnostic path.
`structuredClone` succeeds on values that `JSON.stringify` later
throws on (BigInt, circular refs), so a non-Error cause carrying
either would surface as `payload.cause = clonedObject`, then the
outer `JSON.stringify(payload)` would throw inside the catch handler,
and the FIRST_CHUNK_ERROR chunk never gets emitted. Replaced with
`safeJsonStringify` (BigInt → string, cycles → `[Circular]`) and
route the cause object through `toJsonSafe` so the returned shape is
always plain JSON.
Added tests for both: a wrapped APIError(cause: SyntaxError) yields
parsePosition, and a cause containing both BigInt and a circular ref
still emits a parseable error chunk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The daily-brief hint will start carrying `[name](url)` markdown links so
the AI can resolve referenced entities when the user submits via the
hint. The placeholder layer is the only consumer that wants the visible
label without the link syntax — extract a small `stripMarkdownLinks`
util and apply it at `InputArea/index.tsx` only. `useSend` continues to
forward the raw hint, so the agent still receives the link in the
outgoing message.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(bot): gate device tools by sender identity (LOBE-8715)
External users who @-mentioned a bot ran the agent as the bot owner and
could call LocalSystem / RemoteDevice tools — a confused-deputy hole that
let any group member indirectly read/write the owner's machine.
- `ChatTopicBotContext` carries `senderExternalUserId` + `isOwner`
- `BotMessageRouter` / `MessengerRouter` compute `isOwner` at the entry
point (fail-closed when `settings.userId` is missing)
- `resolveDeviceAccessPolicy` maps sender identity to
`{ canUseDevice, reason }`; trusted-list branch is reserved for future
work without engine changes
- `AgentToolsEngine` gates `LocalSystem` + `RemoteDevice` on `canUseDevice`
- `RemoteDeviceManifest.systemRole` is no longer injected on
external-sender turns — closes the device-list information leak
- Per-call audit log (`lobe-server:agent-device-tool-audit`) at the
dispatch site records sender, isOwner, reason, identifier, apiName
Fixes LOBE-8715
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🚨 chore(bot): replace `any` on botContext / botPlatformContext with concrete types
Picks up the existing `BotPlatformContext` (`@lobechat/context-engine`)
and `ChatTopicBotContext` (`@lobechat/types`) — both already exported —
instead of the inherited `any` placeholders on:
- `OperationCreationParams.{botContext, botPlatformContext, deviceAccessPolicy}`
- `InternalExecAgentParams.botPlatformContext`
- `RuntimeExecutorContext.botPlatformContext`
`deviceAccessPolicy.reason` is now `DeviceAccessReason` instead of `string`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 fix(bot): clear activeDeviceId when canUseDevice=false (LOBE-8715)
The previous patch gated `LocalSystemManifest` in the engine's enabledToolIds,
but `buildStepToolDelta` re-injects local-system from `state.metadata.activeDeviceId`
on every step regardless of whether the engine excluded it. Auto-activation
in `aiAgent.execAgent` populated `activeDeviceId` whenever
`(discordContext || botContext) && onlineDevices.length === 1`, so an
external bot sender with one device online could still get local-system
tools against the owner's device.
- `aiAgent/index.ts`: skip `activeDeviceId` derivation entirely when
`canUseDevice` is false. `deviceSystemInfo` short-circuits naturally on
`if (activeDeviceId) {...}`, so no extra change needed there.
- `RuntimeExecutors.ts`: belt-and-suspenders — if
`state.metadata.deviceAccessPolicy.canUseDevice` is false, swallow
`activeDeviceId` before passing to `buildStepToolDelta`, so a future
plumbing bug at the source can't reopen the bypass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 feat(bot): allow device tools on personal-scope platforms (WeChat) (LOBE-8715)
Not every bot platform can identify an owner. WeChat's LobeHub integration
encodes every inbound thread as 1:1 (`packages/chat-adapter-wechat/src/adapter.ts:465`)
and its settings schema has no `userId` field, so `isOwner` is structurally
false on every WeChat turn. The previous policy denied every WeChat call
with `bot-owner-not-configured` — fail-closed but unusable.
This commit treats platforms whose integration is structurally personal-
scope as trusted. WeChat is the only member today; LINE is intentionally
excluded because its adapter handles group/room threads even though its
schema also lacks `userId` — those must be fixed at the schema layer
before being whitelisted.
- New `bot-personal-platform` reason in `DeviceAccessReason`
- `PERSONAL_SCOPE_BOT_PLATFORMS = new Set(['wechat'])`
- Personal-scope check sits AFTER `isOwner` so a future WeChat schema
with a `userId` field still resolves as the more specific `bot-owner`
- Tests: WeChat without isOwner → allow; WeChat with isOwner=true → still
`bot-owner` (more specific wins); regression guard ensuring Discord /
Slack / Telegram / Feishu / Lark / QQ / LINE keep going through the
standard isOwner gate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(engine): opt existing device gate tests into canUseDevice=true (LOBE-8715)
The `LocalSystem` / `RemoteDevice` enable rules now short-circuit on
`canUseDevice` (default `false`), so tests that exercise the
engine-internal gates (`runtimeMode`, `deviceContext`, `clientRuntime`)
must explicitly pass `canUseDevice: true` — otherwise they assert the
right behavior for the wrong reason or fail outright (e.g. the desktop
RemoteDevice-suppression case the reviewer flagged).
- All `LocalSystem` / `RemoteDevice` / `LocalSystem + RemoteDevice` /
`clientRuntime === "desktop" (Phase 6.4)` blocks now set
`canUseDevice: true`.
- The "disable RemoteDevice in bot conversations" test was repurposed:
the dropped `!isBotConversation` clause is now subsumed by `canUseDevice`,
so for a trusted bot caller (canUseDevice=true) RemoteDevice DOES surface.
The original intent — block when caller is untrusted — is captured in
the new `canUseDevice gate` block.
- New `canUseDevice gate` describe block asserts:
1. `canUseDevice=false` blocks LocalSystem even on a desktop caller
2. `canUseDevice=false` blocks RemoteDevice with proxy configured
3. Omitting `canUseDevice` → fail-closed default (deny)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(execAgent): set isOwner=true on device auto-activation tests (LOBE-8715)
These pre-existing tests model an owner using the bot through Discord and
assert that `activeDeviceId` auto-populates when one device is online.
After LOBE-8715, `activeDeviceId` is gated on `canUseDevice` from
`resolveDeviceAccessPolicy`, so a `botContext` without `isOwner: true`
resolves to `bot-external-sender` → `canUseDevice=false` →
`activeDeviceId=undefined`.
Filling out the `botContext` mocks with `isOwner: true` (plus the other
required fields the type now demands) preserves the tests' original
intent while exercising the new gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the `weixin.sogou.com` and `mp.weixin.qq.com` rules from the crawler
URL ruleset since they are no longer needed.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: refresh content baseline from DB on every ingest call
Vercel serverless routes consecutive batches to different Lambda
instances. A warm replica's in-memory `accumulatedContent` only
reflects batches it processed; it has no visibility into batches
handled by other replicas.
The failure pattern (worst when a repo is selected, since CC makes
tool calls early):
1. Lambda A — batch 1 (text "你好!...") → flushBatchContent writes
2. Lambda B — batch 2 (text "...任务。") → restores from DB, appends,
writes longer text to DB
3. Lambda A — batch 3 (tools_calling only, warm state) → its stale
`accumulatedContent` = batch-1 text → persistMainToolBatch Phase 1
writes `{ tools, content: stale-short-text }` → OVERWRITES the
correct longer DB value → content truncated at "你"
Fix: re-read the current assistant message from DB at the start of
every `ingest()` call. Since `flushBatchContent` writes at the end of
every batch, DB is authoritative. The refresh gives each Lambda the
latest flushed baseline, so new text in the current batch extends
the correct full string.
Cost: one extra `findById` round-trip per warm ingest call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ✨ feat: auto-inject GitHub OAuth token into CC sandbox
Previously the GitHub token was only resolved when repos were selected
AND GITHUB_CRED_KEY was explicitly configured in the agent config —
so CC running without pre-selected repos had no GitHub access and had
to ask the user for a PAT manually.
Changes:
- aiAgent/index.ts: always try to resolve the token using key 'github'
(standard LobeHub OAuth connector default); GITHUB_CRED_KEY still
overrides. No longer guarded behind topicRepos.length > 0.
- sandboxRunner.ts: new buildCredsSetupScript() runs before CC starts:
mkdir -p ~/.creds
printf 'GITHUB_ACCESS_TOKEN=%s\n' <token> > ~/.creds/env
gh auth login --hostname github.com --with-token
Writes ~/.creds/env in the same format as injectCredsToSandbox(["github"])
so CC can source it in sub-shells. Creds step runs before repo clone step.
- cloudHeteroContext.ts: system prompt now tells CC that GITHUB_TOKEN is
set, gh CLI is pre-authenticated, and ~/.creds/env has GITHUB_ACCESS_TOKEN
with the source/auth recipe for sub-shell usage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: adopt max-length content on DB refresh to guard flushBatch retry
The unconditional DB overwrite in ingest() broke the retry contract:
if flushBatchContent threw after events were already marked in
processedKeys, a retry on the same warm instance would read the stale
(shorter) DB value and wipe the in-memory chunks — which processedKeys
would then skip, losing them permanently.
Fix: only adopt the DB value when it is LONGER than in-memory.
This preserves both behaviours:
- Multi-replica stale (the original fix): DB has more content from
another replica → dbContent.length > in-memory → adopt DB. ✓
- flushBatchContent retry on same Lambda: DB still has the old shorter
value, in-memory has the correct accumulation → keep in-memory. ✓
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix(hetero-agent): disable Claude Code AskUserQuestion to avoid auto-decline
CC's built-in AskUserQuestion self-injects an `is_error: "Answer questions?"`
tool_result inside the CLI in `-p` non-interactive mode before the host can
surface the questions, so the model falls back to plain-text prompting after
a wasted round-trip. Add `--disallowedTools AskUserQuestion` to both spawn
sites (desktop driver + lh hetero exec) so the model goes straight to text.
To be revisited once a local MCP-backed replacement is wired to LobeHub's
intervention UI.
* ♻️ refactor(hetero-agent): share CC base args, opt-in partial deltas
- Promote CLAUDE_CODE_BASE_ARGS in `@lobechat/heterogeneous-agents/spawn` to
the canonical source of truth for invariant CC CLI flags (`-p`, stream-json
IO, `--verbose`, `--disallowedTools AskUserQuestion`); export it so the
desktop driver can compose on top instead of duplicating.
- Pull `--include-partial-messages` out of the base. It's now a
`SpawnAgentOptions.includePartialMessages` flag, off by default so
`lh hetero exec` standalone/sandbox runs don't pay for delta noise they
don't render. The desktop driver opts in (chat bubble streams live).
- Permission mode stays caller-specific: desktop hardcodes bypassPermissions
(always user-mode), the package keeps its root-vs-user branch for cloud
sandbox.
* 🎨 style(hetero-agent): pass spawn-args builders an options object
Positional list grew to four args with mixed types — switch to a single
`BuildSpawnArgsParams` object so call sites read by field name and adding
future per-agent flags doesn't push every other caller around.
* 🐛 fix(local-system): guard readFile against binary blobs and oversized output
Previously `lobe-local-system.readFile` would happily decode any extension
as UTF-8 and return the entire content. Reading a 27KB base64-encoded git
bundle blew up the next LLM call to 3.28M tokens / 416s and triggered a
DB rollback. The default 200-line cap was bypassed because base64 was a
single very long line.
Add four layers of protection in `readLocalFile`:
- Hard-reject extensions outside the text-readable + special-parser
whitelist with a structured error pointing the agent at runCommand.
- Sniff the first 8KB and refuse files that look binary (null bytes or
>30% non-printable chars).
- 10MB hard size cap before the file is read into memory.
- Cap each returned line at 8K chars and total output at 500K chars,
with `truncated` / `linesTruncated` flags surfaced in the result.
Refs LOBE-8703.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(file-loaders): preserve UTF-16 text files without a BOM in binary sniffer
The binary sniffer rejected UTF-16LE/BE files that lacked a BOM because
their alternating 0x00 bytes tripped the null-byte heuristic. `TextLoader`
already has a `detectUtf16NoBom` heuristic for these Windows-style exports;
extract it to a shared `detectUtf16` util and run it in the sniffer before
the null-byte check, decoding with the matching variant for the printable
ratio test instead of declaring the file binary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(local-system): render WriteFile new files as a unified diff
Switch the WriteFile render from a syntax-highlighted preview to a
synthesized "new file" unified diff via PatchDiff, matching the
EditLocalFile visual. Markdown files keep their rendered preview.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(local-system): exercise readFile / readFiles end-to-end
The previous LocalFileCtr.readFile / readFiles tests deep-mocked
node:fs/promises and @lobechat/file-loaders. Since the controller is a
thin pass-through to readLocalFile, the assertions ended up testing
shell internals (already covered in packages/local-file-shell), and
broke as soon as readLocalFile gained new pre-flight checks.
Move them into a sibling LocalFileCtr.readFile.test.ts that runs
against a real tmpdir + real file-loaders, so adding more upstream
guards no longer requires touching this suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(siliconcloud): sync models with API, fix duplicates, adjust reasoning params
* 🐛 fix(siliconcloud): fix GLM-4.7 checkModel casing to match model ID
* 🐛 fix(database): attach error listeners to Neon/Node pools to prevent Lambda crash
NeonPool (and NodePool) inherit pg.Pool semantics: when a backend connection
drops on an idle client the pool emits 'error'. With no listener Node
escalates that into uncaughtException — on Vercel this killed the entire
Lambda process (exit 129) and produced a 1805-crash avalanche in 5 minutes,
spiking Neon connection count from 30 to 330+ as half-closed sockets
accumulated (LOBE-8704).
Primary fix: attach `.on('error', ...)` to both pool variants in
`packages/database/src/core/web-server.ts` so the error is logged but
swallowed; the pool recovers on its own per pg docs.
Defense in depth: register `uncaughtException` / `unhandledRejection`
handlers in `instrumentation.ts` (gated to nodejs runtime) so any future
unhandled error doesn't take down the process either.
Refs: https://node-postgres.com/apis/pool#error
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔧 chore: drop process-wide uncaughtException handler
Per review on #14606: the catch-all listener in instrumentation.ts swallowed
every uncaughtException / unhandledRejection — not just NeonPool errors —
leaving the process in an undefined state instead of letting the platform
restart it, and would mask future production bugs.
LOBE-8704 is fully addressed by the targeted pool listeners in
packages/database/src/core/web-server.ts; the broad backstop is unnecessary
and unsafe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): forward pluginState through gateway client tool result
Gateway-mode client tool results lost the `state` field at three points:
the toolResult Zod schema didn't declare it (silently stripped by safeParse),
the ToolResultPayload interface didn't carry it, and projectToExecutionResult
didn't return it. As a result the "技能状态" tab was always empty for tools
dispatched via Agent Gateway, even though clients send `state` correctly and
non-gateway paths persist it as `pluginState`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(prompts): suppress redundant `Exit code: 0` tail in command result
For successful runs, "Command completed successfully." already conveys
the same signal — appending "Exit code: 0" was just noise the LLM had
to skim past. Non-zero exit codes (130 SIGINT, 137 OOM, etc.) keep the
line so the diagnostic information remains available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(prompts): treat non-zero exit code as command failure in result header
`success` is the envelope ("the service responded") and `exitCode` is the
command's own status — they're independent. With `success: true` +
`exitCode: 137` the prior format rendered "Command completed successfully."
on top of a SIGKILL/OOM, lying to the LLM.
Now the header is derived from both: any non-zero exit folds the message
into the failure branch as "Command failed with exit code N[: error]".
The trailing "Exit code: N" line is gone — the same info now lives in the
header, so success rendering is also free of the redundant zero tail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: home daily brief with linkable welcome + paired input hint
Add a per-user "daily brief" surface to the home page. A cron-driven
backend (in the cloud repo) writes paired { welcome, hint } entries
into Redis under `aiGeneration:home_brief:{userId}`. This change exposes
that data through:
- `RedisKeys.aiGeneration.homeBrief` key builder
- `home.getDailyBrief` lambda router query that reads the cached payload
- `homeService.getDailyBrief` client and `useHomeDailyBrief` hook with
shared rotating index via `useSyncExternalStore`
- `WelcomeText` runs a custom typewriter (supports real `\n` line breaks
and parses inline `[label](url)` markdown links so cached entity
references become clickable; falls back to the i18n welcome list)
- `InputArea` shows the matching hint as the chat input placeholder
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: extract daily-brief Redis read into HomeService
Mirrors the AgentService pattern: the lambda home router was reaching
into Redis directly, which mixed I/O concerns with the routing layer.
Move the read into a dedicated `HomeService` so future home-page reads
have a clear home and the router stays thin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: keep WelcomeText typewriter index in sync with shared store
Before: DailyTypewriter held its own `sentenceIndex` state, separate
from the module-level `currentIndex` in `useHomeDailyBrief`. After
the home page rotated past the first pair, navigating away and back
remounted the typewriter and reset its local index to 0 — but the
external index stayed where it was. InputArea read the hint at the
stale external index while WelcomeText restarted at pair 0, breaking
the welcome / hint pairing.
Make the typewriter fully controlled: drop the local `sentenceIndex`,
expose `currentIndex` from `useHomeDailyBrief`, and pass it as a prop.
On `pause`, the typewriter just calls `onSentenceComplete` — the
parent flips the shared index, the new prop flows back, the reset
effect re-arms typing for the new sentence. Single source of truth,
remount-safe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(redis): factor JSON cache reads into getJSONFromRedis util
Three call sites were inlining the same "fetch + null-check + JSON.parse
+ try/catch" recipe against a scoped Redis client:
- AgentService.getAgentWelcomeFromRedis
- HomeService.readDailyBriefFromRedis (new)
Move the recipe into a small `getJSONFromRedis<T>` helper next to the
other Redis utilities and have both services delegate to it. Caller
keeps responsibility for resolving the right scoped client (we don't
want to hide the prefix selection inside the helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(home): use live editor content for Enter-to-send guard
When typing into the home input and pressing Enter immediately, the
empty-message guard sometimes wrongly bailed out. The cause: the guard
read the cached `inputMessage` in `useChatStore`, which is populated by
the editor's async `onMarkdownContentChange`. Lexical commits its
update on a microtask after each keystroke, so a fast type-then-Enter
fires the send path before the cache catches up.
`SendButtonHandler` already passes `getMarkdownContent` through — read
it instead, falling back to the cached value if the handler is invoked
without it. Also propagate the live message into all `inputActiveMode`
branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(home): accept daily-brief hint as the message on empty Enter
Press Enter on the empty home input → send the currently displayed
daily-brief hint as the message (smart-compose / Tab-to-accept style).
Trims the cosmetic trailing ellipsis and rotates the carousel so the
next press picks up a different pair.
Falls through to the previous "no content, skip" path when there's
neither a typed message nor a hint to use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(home): scope daily-brief SWR key + rotation index by userId
The SWR key was a constant string, so an account switch within the same
SPA session — sign out + sign in as another user, or a multi-account
swap that keeps `isSignedIn` true — could surface the previous user's
cached pairs from the same slot. The keyspace in Redis is per-user,
so the served data leaks personalization.
Include the resolved userId in the SWR key, and reset the module-level
rotation index on user change so the new account starts from pair 0
rather than inheriting a stale offset (which could also point past the
end of a smaller pairs list).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: skip reconnect when gateway action already established a connection
Race condition on new-topic first message:
1. switchTopic loads runningOperation → useGatewayReconnect fires
2. executeGatewayAgent calls connectToGateway (status: connecting)
3. reconnectToGatewayOperation overwrites with resumeOnConnect:true
4. Gateway sees resume on a brand-new session → no events → stuck
Second message works because the client store's runningOperation is
stale (from the first op), so SWR deduplications and no reconnect fires.
Fix: bail out of reconnectToGatewayOperation if gatewayConnections
already shows connecting/connected for that operationId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: always pass --cwd /workspace for cloud CC to ensure session resume
CC stores session files at ~/.claude/projects/<encoded-cwd>/.
Without an explicit --cwd the actual working directory can differ
between sandbox invocations, so --resume <heteroSessionId> fails
to locate the previous session files even though the container is
persistent and the ID is correctly stored in topic.metadata.
Default cwd to /workspace for cloud runs (desktop keeps its own
explicit path), guaranteeing a stable session-file location across
page reloads within the same sandbox lifecycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: extend reconnect guard to cover all in-flight connection statuses
The previous guard only skipped reconnect for 'connecting'/'connected'
but the connection can already be in 'authenticating' or 'reconnecting'
by the time useGatewayReconnect fires, leaving the race window open.
Flip the condition: skip for any status that is not 'disconnected'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: restore cold replica state in HeterogeneousPersistenceHandler
Vercel serverless functions are stateless per-request, so `operationStates`
is empty on every `heteroIngest` call. loadOrCreateState always cold-creates.
#14539 fixed `toolMsgIdByCallId` restoration but left `accumulatedContent`,
`toolState.payloads`, and `toolState.persistedIds` empty on cold load,
causing two bugs:
- Content truncation: cold instance starts with `accumulatedContent=''`,
accumulates only the current batch's text, then writes that shorter string
on the next step boundary or terminal — overwriting the longer content the
previous write had already stored in DB.
- Tool duplication / tools[] overwrite: `persistedIds={}` on cold load
means every `tools_calling` event re-creates already-persisted tool
messages, and `payloads=[]` means phase 1/3 writes only the current
batch's tools, wiping previous tools from `assistant.tools[]`.
Fix: in `loadOrCreateState`, fetch the current assistant message and restore
`accumulatedContent`, `accumulatedReasoning`, `toolState.payloads`, and
`toolState.persistedIds` from it. Cold load is now equivalent to warm load.
Also adds two regression tests covering the cold-replica scenarios.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
💄 style(QueueTray): use visible divider color between queued messages
The previous `colorBorderSecondary` rendered the divider effectively
invisible on the elevated dark surface. Switch to `colorFillTertiary`
so stacked queued messages have a perceptible separator.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add signOperationJwt with 4h expiry for hetero-agent operations
- Add `signOperationJwt(userId)` to internalJwt.ts with 4h expiry and
`purpose: 'hetero-operation'`, so Claude Code / Codex tasks running
beyond 5 minutes no longer hit 401 on heteroIngest / heteroFinish
- Update `execAgent` hetero path to use `signOperationJwt` instead of
`signUserJWT`; gatewayToken continues to use 5m `signUserJWT`
- Add unit tests in `__tests__/internalJwt.test.ts` with correct mocks
for `jose` (SignJWT class + importJWK) and `authEnv`, covering all
three signing functions and the expiry difference assertion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔒 security: restrict hetero-operation JWT scope to heteroIngest/heteroFinish
A leaked 4-hour sandbox LOBEHUB_JWT must not be replayable against any
other authenticated lambda route.
- Forward `purpose` claim from JWT payload through validateOIDCJWT →
tokenData → oidcAuth context so middlewares can inspect it
- oidcAuth: reject tokens with purpose 'hetero-operation' — they cannot
reach any normal authedProcedure route
- New heteroOperationAuth middleware: exclusively accepts
purpose 'hetero-operation' tokens, rejects all others
- Export heteroAuthedProcedure (baseProcedure + heteroOperationAuth +
userAuth) from trpc/lambda/index.ts
- heteroIngest / heteroFinish now use heteroAgentProcedure built on
heteroAuthedProcedure + serverDatabase + HeterogeneousAgentService
- Tests: heteroOperationAuth (4), oidcAuth (4), update heteroIngest
test caller to supply purpose:'hetero-operation' context (23 total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix(agent-runtime): recover malformed tool_call names instead of finishing silently
When an LLM emits tool_call names without the `____` separator (e.g. `activateTools`
instead of `lobe-activator____activateTools`), the resolver dropped them silently and
the harness finished with "completed without tool calls" — empty assistant bubble,
no error in dashboards.
Three layers of defense:
- Resolver fallback: when the bare name uniquely matches an API across known
manifests, recover the identifier; ambiguous matches still drop to avoid
false binding.
- StreamingHandler logs unresolved tool_call names so the silent-drop path is
observable in debug output.
- GeneralChatAgent surfaces the unresolvable count and names in reasonDetail
so dashboards can distinguish this from a genuine no-tool completion.
Fixes LOBE-8696
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): restrict bare-name fallback to tools offered this turn
Address review feedback on the LOBE-8696 resolver fallback. The
manifests map passed to ToolNameResolver.resolve is broader than the
tools actually sent to the LLM (the client builds it from every
installed plugin and every builtin; the server can preserve manifests
even after a step deactivates a tool). Without a turn-scope
restriction:
- A model returning a malformed bare name could resolve to a tool that
was not enabled for this turn.
- A disabled duplicate API name could shadow the enabled call and make
it look ambiguous, dropping a valid call.
Pipe an `offeredToolNames` list (the names actually sent in this LLM
payload) into resolve(): when set, the missing-prefix fallback only
considers manifests whose generated tool name appears in the list.
- ToolNameResolver.resolve gains an optional `offeredToolNames` param.
- internal_transformToolCalls forwards the list through.
- createAgentExecutors builds resolvedAgentConfig before the
StreamingHandler so the closure can bind the offered names — same
list that gets sent to the model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context
- Add CloudRepoSwitcher component (web-only multi-select repo picker)
- Pre-topic selections buffered in module singleton (pendingTopicRepos)
- Consumed by gateway.ts at topic creation time via appContext.initialTopicMetadata
- Eliminates race condition where updateTopicMetadata dropped silently
- Extend ChatTopicMetadata with repos[] field for multi-repo binding
- Add initialTopicMetadata to ExecAgentAppContext so repos are written to
topic metadata at creation time (server-side, zero race condition)
- Extend ExecAgentSchema Zod schema with initialTopicMetadata
- Inject GITHUB_TOKEN env var into sandbox so CC can use git/gh CLI
- Build cloudHeteroContext with GitHub auth section when token is available
- Add workingDirectory selector for web (repos[0] fallback)
- Add refreshTopic call in gateway path after new topic creation
- Add CloudHeterogeneousConfig profile editor for GITHUB_REPOS / GITHUB_CRED_KEY
- Extend sandboxRunner with repo clone setup script and systemContext support
* 🐛 fix: add open-source stub for pendingTopicRepos to fix Vite build
* ♻️ refactor: move pendingTopicRepos real impl into submodule, remove cloud override
* 🐛 fix: consume pendingTopicRepos only after topic creation succeeds
* 🐛 fix: add missing getPendingTopicRepos import in gateway
* 🔒 fix: address security and dead-code issues from PR review
- sandboxRunner: sanitize repo dir name to prevent shell injection
- sandboxRunner: use git insteadOf (-c flag) so token is never stored in .git/config
- cloudHeteroContext: fix return type from string|undefined to string (dead branch)
- CloudRepoSwitcher: remove unreachable empty-list branch in popover content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 💬 i18n: add claude setup-token hint to token description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: remove incorrect web hetero→gateway forced routing in agentDispatcher
On web, heterogeneousProvider is ignored — routing falls through to isGatewayMode.
Cloud CC only runs when gateway mode is enabled; gateway.ts handles sandbox
spawning when it detects a hetero provider.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: restore web hetero→gateway routing; update stale test
On web, a configured heterogeneousProvider always routes to gateway —
the cloud sandbox is the only execution environment regardless of
isGatewayMode. The test assumed the pre-cloud-CC world where web
ignored hetero providers entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 📝 docs(version-release): enforce git-derived PR refs and metrics
Add the skill's first-class hard rules for computing release-note inputs
from git instead of memory: latest-tag base via `git describe`, PR refs
from commit subjects, metric counts from `wc -l`, handle resolution via
`gh pr view`, and a pre-publish `comm -23` diff that must be empty.
Also adds @cy948 to the team roster and notes Tsuki / René Wang's
commit-author aliases so contributor classification stops drifting.
* ♻️ refactor(version-release): split skill into router + per-flow references
SKILL.md was 426 lines covering three distinct flows. Split it so each
flow lives next to its own checklist:
- reference/minor-release.md — minor workflow (lifted from SKILL.md)
- reference/patch-release-scenarios.md — patch flows (existing)
- reference/release-notes-style.md — long-form changelog standard,
template, and Computing Inputs hard rules (lifted from SKILL.md)
SKILL.md now reads as a router (~100 lines) with shared CI trigger
rules, post-release automation, precheck, and hard rules. Cross-links
between references replace the previous in-file jumps. Also fixes a
prettier-mangled redirect (`< some-pr-by-them >`) by using a `$PR`
variable instead of an angle-bracket placeholder.
* 📝 docs(version-release): add Hotfix and DB Migration variants to release-notes-style
The Canonical Structure was implicitly long-form (Minor / Weekly), and
hotfix authors had to read `changelog-example/hotfix.md` to learn it
existed. Make the divergence explicit:
- New § Variants for Shorter Releases describes Hotfix structure
(Scope / What's Fixed / Upgrade / Owner) and DB Migration structure
(Migration overview / Operator impact / Rollback) as overrides of the
canonical long-form layout.
- Renamed the canonical section to "Canonical Structure (Long-Form:
Minor / Weekly)" so the boundary is visible.
- Added Hotfix entry to Release Size Heuristics.
- Added a Hotfix subsection to Quick Checklist so the verification
gates differ from long-form (no metric line / no Contributors / Owner
resolved via gh).
`pnpm init` writes `devEngines.packageManager: { version: "^11.0.9" }`
into the generated package.json. corepack@latest rejects ranges in this
field with "Invalid package manager specification ... expected a semver
version", causing the subsequent `pnpm add pg drizzle-orm` to exit 1.
Skip init and write a minimal package.json directly so corepack has
nothing to validate.
* 🐛 fix: sanitize sensitive comments and examples from production JS bundle
- Replace app.example.com with RFC 2606 example.com in agent-browser skill content
- Replace password-stdin examples with interactive auth prompts
- Remove hardcoded password-like strings from code examples
- Reword flagged code comments in page-agent system role
Addresses TAC Security CASA Tier 2 DAST Info findings:
Information Disclosure - Suspicious Comments (CWE-615)
The flagged strings appeared in SPA production bundles:
- /_spa/assets/chat-*.js
- /_spa/assets/index-*.js
* 🐛 fix: revert --interactive to --password-stdin in auth vault examples
The --interactive flag does not exist in agent-browser CLI (only --password
and --password-stdin are supported). Using --interactive would cause auth
save to fail and block login workflows.
Reverted both auth vault examples to use echo | --password-stdin pattern,
which pipes the password via stdin — the recommended secure approach.
* ✨ feat(task): add stop run action to activity card menu
Surface the existing cancelTopic flow in the task detail activity card so
users can interrupt a running topic without opening the chat drawer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): confirm before stopping a running topic
Wrap the new Stop run action in a confirmModal so an accidental click can't
silently abort an in-flight run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(spa): register /tasks and /task in SPA proxy matcher
Without these matcher entries, the Next.js middleware never rewrote /tasks
and /task/:taskId to the SPA catch-all, so the activity feed entries 404'd
in production builds even though the routes were wired in the SPA router.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Only show the skip-and-switch footer when all conditions are met:
AGENT_ONBOARDING_ENABLED, not desktop, server config initialized,
and runtime enableAgentOnboarding flag is on
- Fix typo: swichMode → switchMode
- Expand tests with hoisted mocks covering each visibility condition
* 🐛 fix(security): remove /webapi/proxy and dead URL-manifest plugin code
Closes#14530. The /webapi/proxy endpoint was an unauthenticated open
HTTP proxy. All client callers were dead except NewAPI provider's
browser-side pricing fetch, which now silently falls back to no-pricing
since `parsePricingResponse` already handles non-OK responses.
Removes:
- /webapi/proxy route + API_ENDPOINTS.proxy
- toolService.getToolManifest (+ packages/utils/src/toolManifest.ts)
- src/features/PluginDevModal/UrlManifestForm.tsx
- uploadService.getImageFileByUrlWithCORS
- non-MCP branch in customPlugin reinstall (silently returns for
legacy URL-manifest plugin data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 chore(model-runtime): drop /webapi/proxy hop in NewAPI pricing fetch
The browser branch routed pricing requests through /webapi/proxy to bypass
CORS. Now that the proxy is removed, fetch the upstream pricing endpoint
directly — if CORS or any other error blocks it, fall through to the
existing null fallback (NewAPI just renders without enriched pricing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(model-runtime): drop console.debug assertion in NewAPI pricing fetch
The pricing-network-error case used to assert that console.debug was
called; with the log removed, just assert the graceful fallback (no
pricing on the resulting model). Also tightens an adjacent
branch-coverage test that ESLint flagged for a useless assignment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: recover hetero persistence state across Vercel replicas
Three-part fix for multi-replica split-brain on Vercel serverless:
1. Flush accumulated content to DB after every ingest batch so a
replica switch mid-accumulation doesn't lose text chunks.
2. Persist `heteroCurrentMsgId` to topic.metadata on every step
boundary so new replicas restore the correct currentAssistantMessageId.
3. Restore toolMsgIdByCallId from DB on state creation so tool_results
landing on a different replica than their tool_use are still matched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add the test fixed
* fix: slove the some topic problem
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ♻️ refactor(onboarding): extract language and privacy as shared prefix steps
Move the language-selection and privacy/telemetry consent out of the classic
flow into a shared prefix that runs at /onboarding before branching into either
the agent or classic experience. Welcome decoration is merged with language
selection on a single screen, dropping the total step count by one.
Shared-prefix completion is derived from raw stored settings
(s.settings.general.responseLanguage and telemetry), so no new schema fields
are introduced and existing consumers that rely on the merged-default
telemetry value are unaffected.
Branch routing remains automatic (feature flag + isDesktop check) and is now
encapsulated in deriveOnboardingBranchPath. Both branch routes guard against
entering before the shared prefix is complete.
MAX_ONBOARDING_STEPS drops from 5 to 3 (FullName, Interests, ProSettings).
* ♻️ refactor(onboarding): use original Telemetry + ResponseLanguage as shared steps
Revert the merged welcome+language design. The shared prefix now reuses the
original two classic steps as-is:
- Step 1: TelemetryStep (welcome decoration + privacy/telemetry consent)
- Step 2: ResponseLanguageStep (language selection)
Also suppress the mode-switch + skip footer on the bare /onboarding path so
it only appears once the user has entered the agent or classic branch.
* 🐛 fix(onboarding): persist shared-prefix step in URL to survive locale-triggered remounts
Use react-router's useSearchParams to keep the active shared step in the URL
(?step=2). Local useState was lost when switching language for the first time
because i18next's first-time resource load triggers a remount up the tree;
the URL param survives any remount.
* 🐛 fix(onboarding): unblock branch redirect when user accepts default telemetry
Derive commonStepsCompleted from responseLanguage alone. setSettings strips
fields whose value matches DEFAULT_COMMON_SETTINGS, so accepting the default
telemetry: true left s.settings.general.telemetry undefined and the derive
selector never flipped to true — the redirect to the branch never fired.
Step 2 (language) implies step 1 was completed because the flow is sequential,
so checking responseLanguage alone is sufficient and robust against the
default-strip behavior.
* 🐛 fix(onboarding): redirect after step 2 by deriving completion from responseLanguage only
setSettings strips fields that match defaultSettings, so writing
telemetry=true (the default) never persists to s.settings.general.
That made commonStepsCompleted permanently false even after the user
finished both steps, blocking the redirect to the branch flow.
Drop telemetry from the derive check. Step 1 completion is already
tracked via the URL ?step=2 marker; step 2 completion is the only
event that needs to flip commonStepsCompleted, signalled by writing
responseLanguage (which always differs from the default since
DEFAULT_COMMON_SETTINGS has no responseLanguage entry).
* 🔨 chore(scripts): add reset-onboarding script for redoing the flow
Takes an email, clears users.onboarding, agent_onboarding, full_name,
interests and removes responseLanguage + telemetry from
user_settings.general so the user re-enters the shared-prefix
onboarding from step 1.
Usage:
pnpm workflow:reset-onboarding <email>
bunx tsx scripts/resetOnboarding/index.ts <email>
* 🐛 fix(signup): add refs for email and password inputs to improve focus handling
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(onboarding): skip responseLanguage auto-fill while onboarding is in progress
useInitUserState's onSuccess callback auto-fills general.responseLanguage
from navigator.language whenever the field is missing. For new users
this fired immediately after signup, which made commonStepsCompleted
(which derives from responseLanguage being set) flip to true on first
load, and CommonOnboardingPage's early-redirect skipped past the shared
prefix straight into /onboarding/agent.
Gate the auto-fill on onboarding.finishedAt or agentOnboarding.finishedAt
being set, so legacy users who finished onboarding without
responseLanguage still get the safety-net detection, but in-progress
users keep the field undefined until they explicitly choose it on the
language step.
* 🐛 fix(onboarding): refresh welcome message locale until conversation starts
ensureWelcomeMessage previously only created the welcome on first call
and skipped on subsequent ones, leaving stale welcomes locked to the
locale that was active when the topic was first created. After the
shared-prefix refactor users pick their language earlier than they
used to, so the welcome that was generated during the auto-detect
phase never gets re-translated.
Now the welcome content is rewritten in-place to match the current
responseLanguage as long as no user reply has been recorded yet
(message count <= 1). Once the conversation has started, the welcome
is left as part of the chat history.
* 🐛 fix(onboarding): update welcome message handling to render client-side and avoid persisting during onboarding
Signed-off-by: Innei <tukon479@gmail.com>
* Refactor onboarding user profile handling: remove responseLanguage field
- Removed responseLanguage from SaveUserQuestionInput and related schemas.
- Updated onboarding logic to no longer save or request responseLanguage.
- Adjusted related components and services to reflect the removal of responseLanguage.
- Enhanced user info handling to include displayName and fullName from OAuth.
- Updated tests to align with the new onboarding structure.
Signed-off-by: Innei <tukon479@gmail.com>
* refactor(onboarding): update locale handling to use i18n's resolved language
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(onboarding): remap legacy 5-step classic currentStep on shared-prefix mount
Mid-flow legacy users with persisted currentStep authored under the old
5-step classic flow (Telemetry, FullName, Interests, Language, ProSettings)
would silently skip required profile steps after the renumbering: old
step 2 (FullName) rendered Interests, old step 3 (Interests) rendered
ProSettings. Apply a one-time remap (2->1, 3->2, >=4->MAX) when Common
mounts, gated by isUserStateInit and onboarding.finishedAt absence so it
fires only for in-flight legacy users. Idempotent for new-schema values.
* refactor(onboarding): implement AGENT_ONBOARDING_ENABLED master switch for onboarding flow
Signed-off-by: Innei <tukon479@gmail.com>
* refactor(onboarding): standardize AGENT_ONBOARDING_ENABLED naming in tests
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 🔥 chore: remove agent_task feature flag and graduate task feature
Drop the agent_task / enableAgentTask gate that was guarding the agent
task rollout. The feature is now permanently enabled, so all flag
checks, disabled-state redirects, and disabled-only fallback UI
(SuggestQuestions, CommunityAgents) are removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): create regular task instead of cron job from template card
The "Add task" button on DailyBrief recommendation cards was creating an
agentCronJob (scheduled recurring job). Switch to taskService.create via
the createTask store action so it creates a one-off inbox task and
refreshes the task list, matching user expectation that the click adds
a task rather than a schedule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): support schedule fields on task.create
The brief recommendation card needs to create a recurring scheduled
task in one shot (template carries `cronPattern`). Extend `task.create`
to accept `automationMode`, `schedulePattern`, `scheduleTimezone`, and
thread them through the service + store action. The model already
accepts these via NewTask, and the central schedule-dispatch sweep
picks the task up once status is dispatchable.
TaskTemplateCard now creates a schedule-mode task with the template's
cron pattern and the user's local timezone, restoring the recurring
behavior previously provided by AgentCronJob.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 i18n(home): shorten brief.title from "Daily brief" to "Brief"
Daily-frequency tasks are no longer the only source feeding the section
(scheduled, manual, and on-demand briefs all flow through it now), so
the more general label fits better.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task-list): show skeleton instead of blank while task list loads
Both the list view (TaskList) and kanban view (KanbanBoard / KanbanColumn)
returned null until isInit, leaving the page empty during the first SWR
fetch. Render a TaskItemSkeleton (default + compact variants) to keep the
layout stable and signal that data is loading.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(git-status): toggle review panel on diff-stat click
Clicking the diff-stat chip used to always open the review panel — if
the panel was already showing review, the click was a no-op. Switch to
a toggle: clicking again with the review tab active closes the panel,
matching the implicit expectation that the chip is the entry/exit
control for that view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(brief): update TaskTemplateCard test for createTask flow
Card now calls useTaskStore.createTask with schedule fields instead of
agentCronJobService.create. Replace the agentCronJob service mock with
a useTaskStore mock exposing createTask, and assert the schedule-mode
payload (automationMode + schedulePattern + scheduleTimezone) on the
success path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): jump to task detail after creating from template
The success toast asked users to look in the inbox agent for the new
scheduled task; navigating directly to the task detail is a clearer
landing for what they just confirmed. Drop the toast and route to
`/task/<identifier>` once createTask resolves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeepSeek thinking-mode (deepseek-reasoner / deepseek-v4-*) rejects follow-up
turns when assistant history messages omit reasoning_content. Until now this
was only enforced in the dedicated DeepSeek runtime's handlePayload; users
routing deepseek model ids through any other OpenAI-compatible runtime hit a
400 with "The reasoning_content in the thinking mode must be passed back to
the API."
Move the safety net into convertOpenAIMessages so any OpenAI-compatible call
with a deepseek-named model derives reasoning_content from reasoning.content
and forces an empty placeholder for thinking-eligible models.
Fixes LOBE-8290
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emit agent document tool outcome events from client-triggered agent document tools with tool attribution so hinted skill documents can be observed by Agent Signal.
Hydrate client runtime completion back to the completed assistant message for pre-created assistant turns, allowing same-turn hinted document receipts to match the originating user message.
Harden agent document snapshot reads by falling back to markdown content when stale editor data cannot be projected for decision evidence.
* 💄 style: fill input on follow-up chip click instead of sending
Mirrors the NameSuggestions pattern so users can edit a suggested
follow-up before sending, matching onboarding interaction conventions.
* ✅ test: update FollowUpChips click test for input-fill behavior
Mock updateInputMessage + editor (setDocument/focus) instead of
sendMessage and assert the new fill-input flow.
* 💄 style: move branching action into the message "..." menu
Surface "branching" inside the dropdown menu (right after copy) for
assistant, assistantGroup, and user messages, instead of as an inline
toolbar icon gated behind dev mode. Drops the dev-mode bar override and
renames the now-only ACP-related selector binding to isHeteroAgent.
Rewrite the onboarding marketplace install pipeline from a serial per-agent
loop to a parallel pipeline anchored on a batched fork call. Multi-select
in the picker now finishes in roughly four parallel rounds instead of
~5N sequential round-trips.
- forkAgent tRPC now takes { items: AgentForkBatchInput[] } and returns
per-item AgentForkBatchResult (discriminated union, best-effort: a single
failure does not abort the batch). The upstream market endpoint stays
per-id, fanned out via Promise.all on the server.
- installMarketplaceAgents fans out dedupe, detail fetch, and createAgent
steps via Promise.all/allSettled and consolidates into one batched fork.
- ForkAndChat (community single-fork action) wraps its call as a 1-item
batch and unwraps the per-item result.
* refactor: add the cloud hetero execAgent Runtime way
* ✨ feat: support session resume for heterogeneous agents (Claude Code / Codex)
- Expose `sessionId` getter on `SpawnAgentHandle` (read from `AgentStreamPipeline`)
- Pass `sessionId` to `IngestSink.finish()` so CLI reports it via `heteroFinish`
- Server stores `heteroSessionId` in topic metadata after each turn
- Server reads and passes `resumeSessionId` as `--resume` on subsequent turns
- Remove debug `console.log` statements from aiAgent service and sandboxRunner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: slove some bugs
* fix: add the is dev back
* 🐛 fix: add async to handleAgentRunRequest in gatewayConnectionSrv
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ✨ feat: add collapse toggle to onboarding mode switch toolbar
The dev-mode actions pill at the bottom-right of the onboarding page
covered the operation area below it. Add a chevron toggle so users can
collapse the pill down to a single icon button. Collapsed state is
persisted in localStorage so it survives reloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: make name and avatar editable in onboarding intervention card
Lets the user override the agent's proposed identity in-place before
approving — pick a different emoji from the avatar picker, type into
the name field, and the edits flow through registerBeforeApprove ->
onArgsChange so the actual save uses the user's values.
Other changes:
- Title is now derived from the live edit state, so adding a missing
field flips the wording from "I'll update my name" to "I'll update my
name and avatar" without staleness
- Subtitle hint ("如果不满意,可以直接修改名字或头像") tells the user
the card is interactive
- Test covers the edit-flush path: edits to name + emoji are observed
via onArgsChange when the framework triggers the beforeApprove flush
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: redesign intervention approval card as codex-style options
Drops the inline approve / reject button row in favor of a numbered
two-option layout with a single Submit at the bottom-right, mirroring
Codex's approval picker. The reject row's content is the reason input
itself (placeholder doubles as the row label) so users can type a
follow-up instruction in place; reason flows through to the existing
rejectAndContinueToolCall(messageId, reason) action.
Behavior:
- Default selection is approve; arrow keys (↑/↓) and 1/2 switch options
- Enter submits when no input is focused; reject input has its own
Enter / ↑ handlers so typing doesn't hijack the picker
- Window-level shortcuts skip while any input/textarea/contenteditable
is focused, so the main chat composer is never affected
- approvalMode='allow-list' adds a "Don't ask again for similar actions"
checkbox under option 1, replacing the old split-button dropdown
Also tighten the onboarding intervention editHint copy from
"如果不满意,可以直接修改名字或头像" to "你可以直接在下方修改名字或头像"
(positive framing instead of conditional).
i18n changes (default + en-US + zh-CN):
- Add optionApprove, rememberSimilar, submit
- Repurpose rejectReasonPlaceholder as the inline reject row's placeholder
- Drop now-unused approveAndRemember, approveOnce, rejectAndContinue,
rejectTitle keys
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: tighten PickAgents card layout
- Move avatar and title into a single row (cardHeader) so the agent
template title sits next to the avatar instead of below it; description
stays as a multi-line block beneath
- Switch card border from colorBorderSecondary to colorFillSecondary so
the card outline is visible when sitting on the elevated picker panel
- Mirror the row layout in the loading Skeleton so the shimmer matches
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(agent-marketplace): add Inspector for showAgentMarketplace and submitAgentPick
The marketplace tool was previously falling back to the generic raw-args
"等 N 个参数" header. Add per-API Inspectors:
- showAgentMarketplace: title + up to 3 localized category chips
(sourced from existing CATEGORY_LABEL_I18N_KEYS in tool namespace);
overflow shown as +N
- submitAgentPick: title + selected agent count
Wire AgentMarketplaceInspectors into builtin-tools/src/inspectors.ts
under AgentMarketplaceManifest.identifier and export from the package's
agentMarketplace/client surface.
i18n adds (default + en-US + zh-CN tool namespace):
- agentMarketplace.inspector.pickCount plurals
- agentMarketplace.inspector.moreCategories plurals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: rename showAgentMarketplace label to "Assemble agent team"
The agent narrates intent ("组建 Agent 团队" / "Assemble agent team")
rather than describing a UI surface ("打开助手市场" / "Open agent
marketplace"), which reads more naturally in the inspector header
during onboarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: hide chat/page view switcher in agent conversation header
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(agent-marketplace): render picked agent cards from pluginState
Adds a SubmitAgentPick Render that shows a grid of agent cards (avatar +
title + description + "already in library" tag) instead of the raw text
content the LLM consumes. Also wires the framework so custom-interaction
handlers can return structured pluginState alongside toolResultContent.
Framework changes:
- submitToolInteraction(options) now accepts a pluginState field. After
writing toolResultContent, the chat store calls
optimisticUpdatePluginState so the message's structured state is
available to render components (matching how server-executed builtin
tools persist state)
- Cloud-side wrapper in Conversation/store/slices/tool/action.ts
forwards the new field
- customInteractionHandlers.ts SubmitToolInteractionOptions adds
pluginState; handleAgentMarketplaceSubmit returns the install
summaries via pluginState (same shape that built the LLM-facing text)
Marketplace changes:
- InstallMarketplaceAgentSummary gains an avatar field; the install
helper threads marketAgent.avatar through
- New Render/SubmitAgentPick reads pluginState.summaries to draw a
responsive card grid (already-in-library entries dimmed + tagged)
- Wire AgentMarketplaceRenders through the package's
agentMarketplace/client surface and register under
AgentMarketplaceManifest.identifier in builtin-tools/src/renders.ts
Workflow display labels (collapsed grouped tool row):
- Add showAgentMarketplace ("Assembled agent team" / "组建了 Agent 团队")
and submitAgentPick ("Picked agents" / "选好了助手") to
TOOL_API_DISPLAY_NAMES so the collapsed group no longer falls back to
"Show Agent Marketplace" / "Submit Agent Pick" via toTitleCase
i18n adds (default + en-US + zh-CN):
- tool.agentMarketplace.render.alreadyInLibrary plurals + alreadyInLibraryTag
- chat.workflow.toolDisplayName.{showAgentMarketplace,submitAgentPick}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(web-onboarding): add UpdateDocument render with hunk diff
Replace the raw "Updated persona document (id). Applied N hunk(s)."
text with a structured per-hunk diff view rendered from args.hunks
(no executor state changes — args already carry the patches).
For each hunk render a mode label + line range chip and paint the
affected text:
- replace: removed (red border) → added (green border)
- delete: removed only
- insertAt: green block + L<line> chip
- replaceLines: green block + line range chip
- deleteLines: line range chip only (no body)
The total hunk count piggy-backs on the first hunk's label row instead
of getting its own header (the inspector header chip already shows
total + doc type, so a separate render-side header would be redundant).
i18n adds builtins.lobe-web-onboarding.updateDocument.hunkMode.{replace,
delete,deleteLines,insertAt,replaceLines} across default + en-US +
zh-CN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(explorer-tree): introduce generic ExplorerTree component
Scaffold a reusable tree component at `src/features/ExplorerTree/`
built on top of `@pierre/trees`. The component exposes a typed
`ExplorerTreeNode<TData>[]` input (tree or flat+parentId),
path-driven identity hidden behind an adapter, and a minimal
imperative handle (startRenaming, focus, select, setExpanded,
getSelectedIds).
Wired v1 capabilities:
- multi-select (default* + onChange), uncontrolled + ref
- DnD abstracted as `onMove(MoveEvent)` with canDrag/canDrop gates
- declarative right-click menu via `getContextMenuItems` rendered
through the library's `renderContextMenu` slot
- inline rename via `canRename`/`onCommitRename`/`onRenameError`
- trailing row decorations via `getRowDecoration`
- built-in icon set driven by file extensions
Old `src/features/FileTree/` is tagged `@deprecated` so consumers
can migrate gradually (SkillStore, LibraryHierarchy, WorkingSidebar).
No consumers migrated in this PR — that is tracked as a follow-up.
Design spec: docs/superpowers/specs/2026-04-23-explorer-tree-design.md
* 📝 docs: add ResourceManager ExplorerTree refactor design
* ♻️ refactor(explorer-tree): use id-based tree contracts
* ♻️ refactor(explorer-tree): narrow transitional tree types
* ♻️ refactor(explorer-tree): align transitional prop contracts
* ♻️ refactor(explorer-tree): remove future-only transitional types
* ♻️ refactor(explorer-tree): support controlled id state
* 🐛 fix(explorer-tree): suppress controlled sync feedback
* 🐛 fix(explorer-tree): reconcile controlled ids on stable paths
* ✨ feat(resource): add tree snapshot derivation
* ✨ feat(resource): add tree mutation helpers
* 🐛 fix(resource): harden tree mutation rollback boundaries
* ✨ feat(resource): add tree controller
* 🐛 fix(resource): guard tree controller request ordering
* ✨ feat(resource): add tree route and bridge modules
* 🐛 fix(resource): harden tree route bridge boundaries
* ♻️ refactor(explorer-tree): expose row host events
* ♻️ refactor(resource): wire hierarchy to ExplorerTree
* ♻️ refactor(resource): remove global tree store
* 🐛 fix(resource): revalidate tree mutations by source parent
* 🐛 fix(spa): prebundle explorer tree dependency
* ♻️ refactor(sharedRendererConfig): remove unused dependencies '@pierre/trees' and '@pierre/trees/react'
Signed-off-by: Innei <tukon479@gmail.com>
* ♻️ revert(resource): remove business integration, keep ExplorerTree component only
Revert all ResourceManager business integration while preserving the
generic ExplorerTree component implementation:
- Restore ResourceManager component files to canary state
- Restore src/store/tree/ (deleted by integration commit)
- Remove src/features/ResourceManager/tree/ (controller, mutations, bridge)
- Keep src/features/ExplorerTree/ (generic component)
- Keep @pierre/trees dependency in package.json
* ✨ feat(agent): integrate ExplorerTree into agent documents section
- Replace flat document list with ExplorerTree for 'documents' filter tab
- Convert flat AgentDocument[] to tree nodes via parentId/fileType
- Add tree node click handler (navigate/open) and context menu (delete)
- Fix height chain: ResourcesSection flex:1 -> AgentDocumentsGroup -> ExplorerTree
- Style ExplorerTree via --trees-*-override CSS vars (transparent bg, relaxed density, theme tokens)
* ♻️ refactor(resource-manager): remove outdated ExplorerTree design document
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(agent-documents): wire context menu and DnD via base-ui imperative API
- Replace nested antd Menu surface with @lobehub/ui showContextMenu, capturing right-click on the tree host directly so menu actions (rename, create, delete) survive base-ui focus restoration
- Fix DnD root drop by routing canDrop through directoryPath instead of hoveredPath, so dragging a nested file onto empty root no longer treats the hovered file row as the target zone
* ♻️ refactor(DocumentExplorerToolbar): adjust padding styles for better layout
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(useDocumentTreeOps): integrate confirmModal for delete confirmation
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(ExplorerTree): cast through unknown to satisfy antd MenuItem types
* ✨ feat(AgentDocumentsGroup.test): add mock for DocumentExplorerTree and update tests for document count
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
* ♻️ refactor: merge agent-marketplace into web-onboarding package
Move the standalone `builtin-tool-agent-marketplace` package into
`builtin-tool-web-onboarding/src/agentMarketplace/` as a sub-module
to reduce package sprawl and consolidate related onboarding tooling.
Also adds locale-aware fetching for onboarding agent templates:
- Accept optional `locale` param in `getOnboardingFull` TRPC endpoint
- Pass normalized i18next locale from the client fetcher
- Add unit test for locale resolution
* ♻️ refactor: integrate FollowUpChips into ChatItem and update GroupMessage components
Signed-off-by: Innei <tukon479@gmail.com>
* fix: address Codex review feedback for PR #14514
- Make getOnboardingFull input schema optional with default to preserve
backward compatibility for callers that invoke .query() without arguments
- Parameterize SWR cache key by resolved locale to prevent cross-locale
cache pollution in the PickAgents marketplace component
* chore: remove accidentally pushed .kagura directory and add to .gitignore
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 👷 build(database): add messenger tables for IM bot integration
Adds three new tables to support the Messenger feature (Slack / Telegram
/ Discord / Feishu / MS Teams shared-bot integration):
- messenger_account_links: maps a LobeHub user to an IM account per
(platform, tenant); tracks the active agent for `/switch` flows.
- messenger_installations: per-tenant OAuth install records (Slack
workspaces, Feishu tenants, …); stores AES-GCM encrypted bot
credentials and the installer.
- system_bot_providers: deployment-wide App-level bot credentials
(one Discord App / Telegram bot / Slack App per deployment),
replacing the env-var-based config.
All sensitive credentials are encrypted via KeyVaultsGateKeeper, the
same gatekeeper used by `agent_bot_providers`. SQL is idempotent
(`IF NOT EXISTS` / `DROP CONSTRAINT IF EXISTS`) per repo convention.
Includes models with full test coverage. Schema and migration only —
no router / service wiring in this PR.
* 🐛 fix(database): bridge stale messenger_account_links missing tenant_id
Some envs deployed a pre-squash version of the messenger migrations
where `messenger_account_links` was created without `tenant_id` and
used the legacy 2-column unique indexes. CREATE TABLE IF NOT EXISTS is
a no-op on those tables, so the new 3-column unique index then fails
with `column "tenant_id" does not exist` (PG 42703).
Add the same bridge logic the original 0102 migration carried — ALTER
ADD COLUMN IF NOT EXISTS for tenant_id and DROP INDEX IF EXISTS for the
two legacy indexes. Idempotent on fresh DBs.
* Revert "🐛 fix(database): bridge stale messenger_account_links missing tenant_id"
This reverts commit d5232564e4.
* 💄 style: simplify onboarding agent identity intervention card
- Drop redundant "Onboarding approval" eyebrow, "Agent name"/"Agent avatar" field grid, and "Applies to" target chips — the description above already conveys scope, and the avatar+name preview already shows the new identity
- Rephrase title to first-person agent voice ("I'll update my name and avatar") so the card reads as the agent announcing what it will do, not a generic admin form
- Remove the now-dead applyHint line under the avatar
- Prune unused i18n keys (eyebrow / applyHint / name / emoji / targets / targetInbox / targetOnboarding) across default + en-US + zh-CN
- Update webOnboarding intervention test to match the simplified card
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: use field-aware title for partial saveUserQuestion approvals
The manifest routes name-only and emoji-only saveUserQuestion calls through the same intervention as the both-fields case, but the previous title hardcoded "I'll update my name and avatar". An emoji-only approval would over-promise a rename that never happens.
Pick titleNameOnly / titleAvatarOnly / title based on which fields are actually pending; cover all three branches in webOnboarding.test.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: drop redundant scope description from onboarding intervention
The field-aware title already says exactly what's about to change ("I'll
update my name" / "...avatar" / "...name and avatar"); the secondary line
explaining that the change applies to Inbox + the current onboarding chat
was extra reading without new information for someone mid-onboarding.
Remove the description Text + i18n key (default + en-US + zh-CN) and
collapse the now-single-child header Flexbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): keep input loading on through execAgentTask round-trip
The Gateway branch in `sendMessageInternal` completed the parent
`sendMessage` op before awaiting `executeGatewayAgent`, so during the
`execAgentTask` network round-trip no operation was running. The send
button briefly flickered back to "send" until the child
`execServerAgentRuntime` op started.
Move `completeOperation` to after `executeGatewayAgent` resolves —
by then the child op is already running, so loading state never drops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): hand off parent op inside executeGatewayAgent
Make `executeGatewayAgent` accept an optional `parentOperationId` and
complete it the instant phase-1 init finishes — right after the child
`execServerAgentRuntime` op starts and the assistant message is
associated. Previously the caller had to call `completeOperation` after
`await executeGatewayAgent(...)` returned, which was fragile: any future
`await` added between the child startOperation and the function return
would silently extend the parent op's lifetime past phase-1.
Also wires `parentOperationId` through to `startOperation` so the
parent/child lineage is recorded on the new op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): persist agentId so brief cards render the producing agent's avatar
`BriefCard` only renders the agent avatar when the enriched `brief.agent`
is non-null, which in turn requires `briefs.agentId` to be set. Several
brief creation paths (task lifecycle synthesize/error/review, and the
agent-driven `lobe-brief` tool runtime) were inserting briefs without
`agentId`, leaving the avatar slot empty in the Daily Brief card.
Pass `assigneeAgentId` from the task in `TaskLifecycleService` and
`context.agentId` from the tool execution context in the brief runtime.
No backfill — internal testing only, historical rows stay null.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): honor stop clicks during phase-1 init
With the parent `sendMessage` op kept running through the
`execAgentTask` round-trip (so the input loading state stays on),
clicking Stop now reaches `cancelOperation(sendMessage)` mid-await but
`executeGatewayAgent` was unaware of the abort: the request finished,
the server task got created, the WS opened, and the agent ran despite
the cancel.
Fixes:
- Plumb the parent op's AbortSignal into `aiAgentService.execAgentTask`
so the fetch itself aborts in-flight when cancel arrives during the
round-trip.
- After every await in phase-1 init, re-check `signal.aborted` and bail
out — the server task may already exist if cancel arrived after the
request resolved, so fire `interruptTask` best-effort before throwing.
- In the caller catch path, skip `failOperation` when op status is
already `cancelled` so we don't clobber the user-cancelled state with
`failed`.
Adds a regression test that pre-aborts the controller, awaits
`executeGatewayAgent`, and asserts the signal is forwarded, the server
task is interrupted, and the child op / message association / WS
connect / parent completion are all skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(review): add branch-compare diff mode with base ref picker
Introduces a Branch mode in the agent Review panel that diffs the current
HEAD against the remote default branch (resolved via `refs/remotes/origin/HEAD`,
overridable via a per-repo base picker). Pulls the comparison data through a
new `getGitBranchDiff` IPC that streams `git diff base...HEAD` and reuses the
existing per-file split + size-cap path, plus `listGitRemoteBranches` for the
picker. Renders a GitHub-style `base ▾ ← head` label with shrink/ellipsis
behaviour, swaps the loading spinner for `NeuralNetworkLoading`, and persists
the user's base override in localStorage keyed by working directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-profile): hide right-panel toggle for heterogeneous agents
Heterogeneous runtimes (Claude Code, Codex, etc.) own their own toolchain
and don't surface the LobeHub right-panel content, so the toggle button is
a dead-end in their profile header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When assistant content blocks are split into answer and workflow segments,
each segment now receives explicit `contentOverride` and `hasToolsOverride`
props so that the rendered markdown matches the segment's own content
instead of all segments reading the same store subscription.
* ✨ feat(kb-tool): integrate BM25 search and docs_* read for inline documents
- searchKnowledgeBase now returns inline documents (BM25 over documents.content)
alongside file chunks (vector). Inline custom/document records created via
createDocument or `lh kb create-doc` are now discoverable through the agent tool.
- readKnowledge accepts both file_* and docs_* IDs. docs_* reads documents.content
directly (no S3 lookup, no parse).
- chunkRouter.semanticSearchForChat: dual-path with Promise.allSettled — failures
on either path no longer kill the whole call; surfaced via new `errors` field.
- formatSearchResults renders <files> and <documents> sections separately.
Fixes LOBE-8606
Fixes LOBE-8608
* style(TitleSection): add border radius to title input field
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(kb-tool): preserve search-path errors in zero-result responses
When semanticSearchForChat returns no hits but includes errors (e.g. vector
search fails and BM25 finds nothing), use formatSearchResults which renders
error notes, instead of promptNoSearchResults which drops them silently.
---------
Signed-off-by: Innei <tukon479@gmail.com>
Replace the inline `AgentTaskList` card on agent and inbox welcome
screens with a dedicated `Tasks` section in the agent sidebar that
groups items by status (Pending review / Backlog / In progress).
Sidebar fetch is scoped to active statuses only — `done` and
`canceled` are neither pulled nor rendered, and use a separate SWR
key from the kanban page so the two views don't trample each other's
state.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(agent): migrate remaining /api/agent routes to Hono
Move the static `route.ts` handlers under `src/app/(backend)/api/agent/`
into the existing Hono app at `src/server/agent-hono/`, leaving only the
SSE `stream` endpoint as a Next.js route. Behavior, URLs, and auth
semantics are unchanged.
- New middlewares: `qstashAuth` (QStash sig only) and `bearerSecretAuth`
(factory for arbitrary `Bearer <secret>` checks)
- Migrated handlers: `run`, `webhooks/bot-callback`, `gateway`,
`gateway/start`, `gateway/callback`, `webhooks/[platform]/[[...appId]]`
- `gateway/callback` keeps inline auth so the disabled-feature 204 still
short-circuits before any auth check
- `gatewayCron` keeps `next/server`'s `after()` for the 10-min poll loop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧪 test(agent-hono): cover migrated route handlers and new middlewares
Add unit tests for the handlers and middlewares introduced by the
/api/agent → Hono migration. Each test uses the same hand-built Hono
Context stub pattern as `toolResult.test.ts` (vitest can't resolve the
hoisted `hono` package, so a real Hono Context isn't available in
tests).
Coverage:
- middlewares/qstashAuth (sig pass/fail → next called/not, body forwarded
to verifier)
- middlewares/bearerSecretAuth (503/401/200 paths, lazy secret eval)
- handlers/runStep (validation, lock 429 + Retry-After, success shape,
upstash-retried header forwarding)
- handlers/botCallback (validation + service delegation + 500 on throw)
- handlers/gatewayCallback (disabled-feature 204, auth, zod validation,
state.status → BotRuntimeStatus mapping)
- handlers/gatewayStart (start/restart paths, stop-before-ensure
ordering, 500 on failure)
- handlers/platformWebhook (param validation, raw request passthrough)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: polish onboarding agent welcome and name suggestions
- Float NameSuggestions above ChatInput (out of greeting message), match width via WideScreenContainer
- Compact suggestion cards: emoji and name on one row, smaller padding, ellipsis prompt
- Migrate suggestion data from i18n to a typed config (`nameSuggestions.config.ts`) with EN/ZH content
- Expand pool to 50 differentiated names; ZH uses native Chinese names, EN uses English; sample 3 random items per group, refresh excludes current ids
- Click a card to fill ChatInput instead of sending immediately
- Tighten welcome footer copy in EN/ZH
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: refine onboarding name suggestions and click-to-fill flow
- Click a suggestion fills ChatInput via editor.setDocument + focus instead of sending immediately
- Append localized avatar hint ("Use {emoji} as the avatar." / "头像用 {emoji}。") to the filled message
- Expand suggestion pool to 100 with bilingual EN/ZH content; mix 2/3/4-char Chinese names; rebalance emoji↔name pairings; tone the 4-char ZH names toward modern/youthful phrasing
- Update NameSuggestions.test.tsx to mock editor.setDocument/focus and i18n interpolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tools): add web-onboarding tool inspectors and write document render
- Add Inspector components for FinishOnboarding / ReadDocument / SaveUserQuestion / UpdateDocument / WriteDocument under @lobechat/builtin-tool-web-onboarding/client
- Add Render component for WriteDocument
- Wire WebOnboardingInspectors and WebOnboardingRenders into the central builtin-tools registries (inspectors.ts / renders.ts)
- Add tool display names (saveUserQuestion → "Recorded info", writeDocument → "Wrote a document") to AssistantGroup constants and chat locale
- Add plugin locale keys for docType (User Persona, SOUL.md) and pluralized inspector counters (chars / changes / interests); shorten saveUserQuestion API name to "Save"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: guard resolveNameSuggestion against undefined locale
When useTranslation is mocked without an i18n.language (e.g. Conversation.test.tsx), locale came in undefined and resolveNameSuggestion crashed on `.toLowerCase()`. Treat missing/unknown locales as a fallback to en.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): reduce streaming re-renders with reference stabilization and self-subscribing components
- Add stabilizeReferences utility to pin unchanged subtrees to previous identity after parse()
- Make Tool, Tools, and MessageContent self-subscribe via store selectors instead of receiving data as props
- Stabilize handleExpandedChange and expandedKeys in WorkflowCollapse with useCallback/useMemo
- Add selectors: findBlockById, getToolsInBlock, getToolInBlock, getBlockContent, getBlockHasTools
* 🔧 chore(agent-mock): update todo-write-stress test case
* feat: refactor todo-write-stress case to utilize lobe-gtd API for task management and enhance workflow with structured plans and todos
- Updated tool steps to replace previous bash commands and file operations with lobe-gtd API calls for creating and updating todos and plans.
- Introduced structured plans for various phases of the migration process, enhancing clarity and organization.
- Implemented a breathing step to simulate processing between tool-call batches.
- Enhanced the overall flow of the todo-write-stress case to reflect a more realistic and organized task management approach.
refactor: optimize ContentBlocksScroll component with virtualized list for improved performance
- Added CSS styles to enable content visibility auto for off-screen workflow items, preserving React state while optimizing rendering.
- Updated Flexbox component to conditionally apply virtualized list styles based on the variant prop, enhancing layout performance.
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(conversation): remove virtualized list styles to improve rendering performance
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(conversation): address codex streaming review feedback
* ♻️ refactor(conversation): use query structural sharing helper
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): add afterMutateHandler for post-mutation synchronization
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): enhance beforeMutateHandler with context and add meaningful content check
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): improve data source validation and streamline command dispatch logic
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): add test for Page Agent editTitle behavior without sending content or editorData
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): update LiteXML node extraction to include attributes and improve error logging
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix: use namespace import for GeneralChatAgent to fix vi.spyOn in tests
vi.spyOn on a module namespace object requires the production code to
access the class through the same namespace. Destructured imports capture
a direct binding that bypasses the spy, causing "Class constructor
GeneralChatAgent cannot be invoked without 'new'" in tests.
* 🐛 fix: replace vi.spyOn on class constructor with vi.mock for GeneralChatAgent
vi.spyOn wraps a class with a plain function that loses [[Construct]]
semantics in ESM, causing "Class constructor GeneralChatAgent cannot be
invoked without 'new'". Replace with vi.mock + hoisted mock constructor
that properly tracks calls while preserving new-ability.
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 💄 style: update heterogeneous agent ChatInput placeholder text
Change 'Ask {{name}} to do a task...' to 'Describe a task or ask a question to {{name}}' for a more natural prompt consistent with Claude Code style.
* fix: also update TypeScript locale source for sendPlaceholderHeterogeneous
* fix: unify casing for popup window labels and simplify folder chooser text
In execAgent/bot mode, `serverMessagesEngine` is called from
`RuntimeExecutors.ts` without several `{{VARIABLE}}` placeholders that
the client-side `contextEngineering.ts` correctly resolves via stores
and lambdaClient. This caused literal `{{CREDS_LIST}}`, `{{username}}`,
`{{language}}`, `{{memory_effort}}`, `{{sandbox_enabled}}`, and
`{{CRON_JOBS_LIST}}` strings to leak into LLM prompts.
Fix: resolve each missing variable before building `contextEngineInput`:
- `{{username}}` / `{{language}}`: `UserModel.getInfoForAIGeneration()`
- `{{sandbox_enabled}}`: check `lobe-cloud-sandbox` in enabled tools
- `{{memory_effort}}`: read from `agentConfig.chatConfig.memory.effort`
- `{{CREDS_LIST}}`: `MarketService.market.creds.list()` (lobe-creds gate)
- `{{CRON_JOBS_LIST}}`: `AgentCronJobModel.findWithPagination()` (lobe-cron gate)
All fetches are best-effort (try/catch → empty string fallback) so a
transient error never breaks agent execution.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass the user's preferred response language (from settings) to
chainTaskTopicHandoff and chainGenerateBrief so that task run titles
and briefs always output in the user's configured language instead of
following the agent's content language.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: add server runtime for lobe-agent-management tool
- Add `agentManagement.ts` server runtime in `serverRuntimes/`
- Implement all 9 API methods: `createAgent`, `updateAgent`, `deleteAgent`,
`getAgentDetail`, `duplicateAgent`, `updatePrompt`, `installPlugin`,
`searchAgent`, `callAgent`
- Uses `AgentModel` from `@lobechat/database` for agent CRUD
- Uses `DiscoverService` for marketplace search in `searchAgent`
- `callAgent` with `runAsTask: true` returns `execTask` state for task system
- Register `lobe-agent-management` in `serverRuntimes/index.ts`
Fixes LOBE-8434
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: address review feedback for agent-management server runtime
- callAgent: always use task path on server (no `registerAfterCompletion` available for synchronous execution)
- installPlugin: create `user_installed_plugins` DB record via PluginModel so manifest is discoverable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 💄 style(brief-card): mute brief icon when brief is resolved
Resolved briefs now render the leading icon with muted gray colors instead
of the type's accent color, matching the existing "已标记为已解决" pill so the
card visually reads as inactive at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(page-agent): add custom Render for modifyNodes tool
Wires page-agent renders into the central registry and adds a per-operation
list view for modifyNodes (action icon, position chip, litexml preview, and
per-op success/error from pluginState.results), replacing the JSON fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): set trigger='task' on briefs created from task lifecycle
Populate the existing `trigger` column on briefs emitted by the task
lifecycle (error, synthesized topic, auto-review pass/retry/force-pass)
and the heartbeat watchdog (workflow + tRPC), so consumers can filter
briefs by source module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief-card): show only the producing agent avatar
Stop fetching every agent in the task tree for brief cards. The stacked
Avatar.Group looked noisy for tasks with multiple subagents and didn't
convey ownership; render a single avatar for the agent that produced
the brief instead (`brief.agentId`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): add aiAgent.heteroIngest / heteroFinish procedures (LOBE-8535 phase 2a)
Wires `lh hetero exec` producer streams into the existing StreamEventManager
fanout: events flow CLI → tRPC → Redis Stream → gateway WS → renderer with
the same wire shape as gateway-driven runs.
- Reconcile server StreamEvent.type with @lobechat/agent-gateway-client's
AgentStreamEventType so tool_execute / tool_result land natively
- HeterogeneousAgentService skeleton with sequential publish (preserves
stepIndex ordering) + terminal agent_runtime_end fallback on finish
- Inline Zod schemas on aiAgentProcedure; topicId required (operationId
reverse-lookup unreliable per LOBE-8516 design decision)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): add HeterogeneousPersistenceHandler — server-side DB writes (LOBE-8535 phase 2b)
Mirrors src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts
(1.8k lines) for the DB concerns. Renderer keeps its own copy for
desktop-host concerns (IPC, store dispatch, notifications); cloud / CLI
ingest goes through this handler instead.
- 3-phase tool persist: pre-register tools[] → create role:'tool' message
→ backfill result_msg_id (mirrors persistToolBatch lines 319–411)
- Subagent threads: lazy-create on first tagged chunk + per-turn assistant
chaining + finalize on parent tool_result with terminal assistant
- Step boundaries: stream_start { newStep: true } flushes prior content
and chains a new assistant off the last tool message
- Per-turn metadata persistence (step_complete phase=turn_metadata)
- Module-level state map keyed on operationId; idempotency via
(stepIndex, type, timestamp). Multi-replica caveat documented — phase 3
sandbox owns the endpoint per-instance so sticky routing is implicit.
Tests:
- 13 unit tests with fake-models harness covering bootstrap, idempotency,
3-phase persist, step boundaries, subagent lifecycle, terminal events
- 2 fixture-driven tests replaying .heerogeneous-tracing/cc-streaming.json
(502 events, 71 tool uses) end-to-end with idempotency assertions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): restore runtime imports after lint auto-fix
ThreadStatus / ThreadType / AgentRuntimeErrorType are used as values, not
just types — the post-commit linter incorrectly converted the import to
`import type`, which broke the build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): full renderer event-branch parity + session resume (LOBE-8535 phase 2b' + 2c)
Renderer-parity additions to HeterogeneousPersistenceHandler:
- Echo-suppression: when CC streams an AuthRequired error string into
`content` BEFORE emitting the structured error, the assistant ends up
with both. Mirror the renderer's `shouldSuppressTerminalErrorEcho` logic
(lines 113–130 of heterogeneousAgentExecutor.ts) so we keep only the
structured error in those cases. Trigger conditions: `AuthRequired` code
or explicit `clearEchoedContent` flag.
- 34 new branch-coverage tests against every event variant the renderer
dispatches on (step_complete phases, stream_start with/without newStep,
stream_chunk text/reasoning/tools_calling × main/subagent, all no-op
variants, terminal error echo handling, subagent edge cases).
Phase 2c — session id persistence + resume helper:
- ChatTopicMetadata.heteroSessionId docstring updated: it's now the shared
field for desktop and cloud paths (was tagged "desktop only").
- handler.finish() now accepts `sessionId` and writes it via
TopicModel.updateMetadata (merges, preserves runningOperation peer).
- HeterogeneousAgentService passes sessionId through, exposes
`getHeterogeneousResumeSessionId(topicId)` helper for phase 3 cloud
sandbox routing to inject `--resume <id>` on the next CLI spawn.
- 9 tests covering happy path, missing session id, error result still
persists, peer-field preservation, updateMetadata failure isolation,
and the resume helper's lookup paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): collision-safe idempotency key + mark-processed-after-success + portable fixture (PR #14444 review)
Three issues from PR review:
1. **Idempotency key collision** — the old `(stepIndex, type, timestamp)`
triple collided when CC bursts multiple `stream_chunk` events through
the same step within a single `Date.now()` millisecond. Later chunks
got dropped as duplicates → silent assistant truncation. Now keys
include a stable FNV-1a fingerprint of `event.data`, so distinct
payloads stay distinct even at the same timestamp.
2. **Mark-processed-before-handle** — `processedKeys.add(key)` ran BEFORE
`handleEvent`, and ingest swallowed throws. A transient DB error in
any per-event write was silently lost: the event was marked done,
the BatchIngester acked OK, retries skipped it, content was gone.
Now: mark only after successful handling + propagate throws all the
way to the BatchIngester so the batch retries. Idempotency map
dedupes the events that already succeeded earlier in the batch.
Knock-on: removed every `.catch(log)` from per-write paths. Renderer's
"log + continue" posture doesn't fit the server (authoritative for
cloud runs, silent partial writes diverge DB from WS view).
3. **Portable fixture** — `.heerogeneous-tracing/cc-streaming.json` is
gitignored and missing in CI, so the fixture-driven test couldn't run.
Replaced file IO with a synthetic stream that captures the same
characteristics (multi-step, bursty same-millisecond text chunks,
tool_use → tool_result pairs, step boundaries, terminal event). The
synthetic fixture is also more meaningful — it has explicit assertions
about chain-shape and bursty-text dedupe correctness.
Tooling adjustments to support the new contract:
- `persistToolBatch` restructured: payloads de-dup by id (so retries
don't duplicate); `persistedIds` populated only AFTER successful
per-tool create; phase 1 + phase 3 always run (idempotent re-writes)
so a partial-failure retry can complete missed phase 3 backfills.
- `ensureSubagentRun`: thread/user/first-assistant create errors throw
out instead of returning `undefined` and dropping the run.
`ThreadModel.create` already uses `onConflictDoNothing` on id, so
retrying the same generated id is safe.
Tests added (69 hetero-agent tests, was 66):
- Bursty same-timestamp distinct-content text chunks → all preserved
- Mark-processed-after-success retry contract (transient flake recovery)
- Synthetic fixture replays a multi-step CC-shaped run with chain-shape
+ idempotency + partial-batch retry assertions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
♻️ refactor(time): extract useActivityTime hook and move time keys to common namespace
- Add `useActivityTime` hook wrapping `formatActivityTime` with i18n built in
- Move `time.formatThisYear/formatOtherYear/today/yesterday` from `discover` to `common` namespace
- Refactor chat header (hetero-agent), Task Activities, memory/home time, and Comment/Topic cards to use the hook so they show relative time (`5 minutes ago`) within 24h and absolute date afterwards
- Switch `PublishedTime` and `AgentTaskItem` to consume time keys from `common`
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): finalize trace snapshot on error path
Propagated errors from RuntimeExecutors (e.g. `markPersistFatal` from a
parent_id FK violation) used to skip snapshot finalization entirely:
the success-path `finalizeSnapshot` block lived inside the try, so the
catch threw without writing the canonical
`agent-traces/<agentId>/<topicId>/<op>.json`. The partial sat orphaned
at `_partial/<op>.json`, the final S3 path returned 404, and the failed
op was invisible in the trace bucket while still showing as `status:
'error'` in Redis. (LOBE-8533)
Extract the finalize block into `finalizeSnapshotForOperation` and call
it from both the success branch and the error catch. The error call
synthesizes a failed step (the real one never reached
`appendStepToPartial` — it threw before the partial push), so step
counts stay aligned with the assistant message that triggered the call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test: align expected strings with English-only labels and fix mobile router import sort
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): dedupe failed-step append and trust finalized step count
- finalizeSnapshotForOperation now merges the error event into an existing
step record when the synthetic failedStep collides with one already
written by the success-path append (e.g. saveAgentState or queue
scheduling threw post-append). Prevents duplicate stepIndex entries
that corrupt ordering and per-step metrics in trace reconstruction.
- totalSteps is derived from the finalized step array instead of
state.stepCount, so the synthesized failed step is reflected in the
snapshot total (Redis-loaded stepCount lags by one on the error path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): support multimodal input across CLI / shared spawn / desktop
`spawnAgent` and `lh hetero exec` could only take a flat string prompt, so
attaching images required bypassing the shared layer (which is what desktop
actually did). This adds a unified `AgentPromptInput` shape — string sugar or
an array of text/image content blocks — and lifts image handling into the
shared `@lobechat/heterogeneous-agents/spawn/input` module.
Image sources accept URL (with optional id for cache dedupe), local path, or
inline base64. The shared `normalizeImage` fetches/reads/decodes, with
optional on-disk caching keyed by `sha256(id || url)`. `materializeImageToPath`
writes buffers to a cache dir (used by Codex `--image <path>`), with byte-
signature sniffing fallback when MIME is generic. `buildAgentInput` is the
single source of truth for per-agent serialization: Claude Code receives base64
image blocks inline in stream-json; Codex receives text on stdin + repeatable
`--image <path>` flags.
CLI gets three input modes: `--prompt <text>` + `--image <path|url|data:>`
(repeatable), `--input-json <file|->` for full content-block JSON, and stdin
auto-detection (JSON vs plain text by first non-whitespace character).
Mutually-exclusive flag combinations error early.
Desktop's `HeterogeneousAgentCtr` drops ~100 lines of duplicated cache /
sniffing code; helpers (`buildStreamJsonInput`, `resolveCliImagePaths`) become
thin wrappers around the shared functions. Driver interface and IPC contract
are unchanged.
`spawnAgent` is now async (image normalization fetches/reads before spawn).
Verified end-to-end: `lh hetero exec --type claude-code --prompt ... --image
red.png` → CC replied "I see a solid red color." `--input-json` mode also
verified. 28/28 desktop tests, 11/11 CLI hetero tests, 22/22 spawn package
tests pass.
Refs LOBE-8523 (phase 1a follow-up before phase 1b ingest).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔧 chore(cli): include types/model-bank/business-const in workspace
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(types): inline crawler and python-interpreter types
Drop workspace deps on @lobechat/web-crawler and @lobechat/python-interpreter
from @lobechat/types by inlining CrawlSuccessResult / CrawlErrorResult /
CrawlUniformResult and PythonOutput / PythonResult into the relevant tool
type modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔖 chore(cli): bump @lobehub/cli to 0.0.10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(github-tool): prefer description over command in inspector/render header
Show the human-readable `description` arg in the gh tool's collapsed
inspector chip and result-card header when provided; fall back to the
extracted subcommand. Full command is still visible in the expanded
Command code block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): treat generic Content-Type as unknown + handle async spawnAgent failures
Two issues raised on PR #14433 review:
**P1 — generic Content-Type bypassed sniffing in normalizeImage**
`fetchUrlImage` accepted any non-empty `Content-Type` as the final
`mediaType`, so CDN responses defaulting to `application/octet-stream` (or
`text/plain`) skipped URL/byte-based detection and forwarded an unrecognized
type into Claude Code's stream-json `media_type` field — Anthropic rejects
those even when the bytes are a valid PNG/JPEG. The same flaw existed for
base64 sources whose declared `mediaType` was generic.
Introduce `pickImageMediaType(headerType, url, buffer)`: the header value is
preferred only when it's a recognized `image/*` type we know how to extension-
map; otherwise it falls through to URL extension hint → byte-signature sniff
→ raw header → `image/png` final fallback. Applied uniformly to URL fetch,
URL cache hit, and base64 decode paths. Path sources are unchanged (their
"header" is the file extension, which is already authoritative when present).
**P2 — async spawnAgent rejections crashed the CLI**
`spawnAgent` is now async and can reject during image normalization (missing
local `--image` path, fetch failure, decode error). The CLI awaited it
outside any try/catch, so user-input errors surfaced as unhandled rejections
with stack traces instead of the friendly `log.error + process.exit` path
used for prompt validation.
Wrap the `await spawnAgent(...)` in try/catch, log the error message, exit 1
(matching the existing "Stream error from agent process" convention).
**Tests**
- `buildAgentInput.test.ts`: 3 new tests covering octet-stream URL
Content-Type → byte sniff, octet-stream base64 declared type → byte sniff,
generic header + URL extension hint preferred over header.
- `hetero.test.ts`: 1 new test verifying spawnAgent rejection produces clean
`exit(1)` instead of an unhandled rejection.
Manually verified:
`lh hetero exec --image /tmp/does-not-exist.png`
→ `[ERROR] Failed to start agent: ENOENT: no such file or directory…` + exit 1
Refs LOBE-8523.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enable mobile app to access home.getSidebarAgentList for migrating
SessionList from sessionId to agentId (LOBE-8401).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🌐 i18n: add taskDetail.runAll keys for subtask dependency runner
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(cli): add `lh hetero exec` for standalone heterogeneous agent runs (LOBE-8523 phase 1a)
Phase 1a of LOBE-8516: a Node-side `spawnAgent()` plus the CLI command that
drives it. Standalone-only — no `--topic` / `--operation-id` / no server
ingest. Output is `AgentStreamEvent` JSONL on stdout, one event per line.
Why phase 1a is its own milestone: it lets us validate the producer pipeline
end-to-end (`spawn → JsonlStreamProcessor → adapter → toStreamEvent`) under a
plain Node process, get Device-mode + manual debugging unblocked, and ship
without waiting on phase 2's server `heteroIngest` procedures.
## Shared `spawnAgent({ agentType, prompt, resumeSessionId, cwd, command })`
- Lives in `@lobechat/heterogeneous-agents/spawn`. Pure Node — no Electron, no
image cache, no on-disk tracing, no proxy env composition. Desktop main keeps
its own bespoke spawn path for those host concerns; this minimal version is
what the CLI sandbox + terminal use case needs.
- CC: stream-json stdin format + the established preset flags. Codex: `exec` /
`exec resume` form with `--json --skip-git-repo-check --full-auto`.
- Returns `SpawnAgentHandle` with: async-iterable `events`, `exit` promise,
`kill(signal)` (Unix process-group kill, Windows direct), `pid`, raw `stderr`.
- Internally a single-queue async iterator coordinates between the stdout
listeners and the consumer — keeps backpressure simple, no extra deps.
## `lh hetero exec` command
```
lh hetero exec --type claude-code|codex
[--prompt - | --prompt <text>] # default stdin
[--resume <sessionId>]
[--cwd <path>] # default process.cwd()
[--command <bin>] # default `claude` / `codex`
[--operation-id <id>] # uuid v4 generated if omitted
```
- Reads prompt from stdin when omitted or `-`.
- Forwards child stderr to ours so users see auth prompts / missing-binary
errors.
- Ctrl-C → SIGINT to the child's process group (Unix); a second Ctrl-C
escalates to SIGKILL.
- Exit code passthrough: child code 0/non-0 stays as-is; SIGINT / SIGTERM /
SIGKILL map to POSIX 130 / 143 / 137.
## Out of scope (phase 1b — next PR)
- `--topic` / `--operation-id` flags as REQUIRED + the BatchIngester
- `--render none|jsonl` flag (phase 1a is implicit JSONL)
- trpc `aiAgent.heteroIngest` / `heteroFinish` calls
- Gateway WS interrupt subscription
## Validation
- `bunx vitest run packages/heterogeneous-agents` — 113 passing (8 new
spawnAgent tests + the 105 pre-existing on canary)
- `bunx vitest run apps/cli/src/commands/hetero.test.ts` — 7 passing
(all `--type` / `--prompt` / `--operation-id` / exit-code-passthrough /
SIGINT-mapping branches)
- Real end-to-end: `bun src/index.ts hetero exec --type claude-code --prompt
'Reply with exactly the word HELLO and nothing else.'` produced clean
AgentStreamEvent JSONL (stream_start → 2 stream_chunks → step_complete
turn_metadata → step_complete result_usage → stream_end → agent_runtime_end),
every line stamped with the same auto-generated operationId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(spawn): serialize pipeline pushes so flush waits for in-flight chunks
When stdout emits multiple chunks back-to-back — or `'end'` lands while an
earlier `pipeline.push()` is still awaiting the Codex tracker's filesystem
reads — the per-chunk `.then` handlers ran concurrently. Two consequences:
1. Out-of-order events. Push #2's events could resolve before push #1's,
so the JSONL stream came out shuffled.
2. Late-event loss. `'end'` would call `pipeline.flush()` and immediately
set `streamEnded = true` while prior pushes were still pending. The
async iterator could then return `{ done: true }` before those pushes
queued their events.
Fix: thread every `push()` / `flush()` / error-surface call through a single
`pipelineQueue` `Promise` chain, the same shape the desktop controller uses
for its broadcast queue. `flush()` now reliably runs after every queued
push has drained, so `streamEnded` is the very last write.
Two regression tests cover the failure modes by spying on
`AgentStreamPipeline.push` to inject deterministic delays:
- "preserves event ordering across async pipeline.push() calls" — chunk A
resolves slower than chunk B; without the chain B arrives first.
- "iterator drains slow in-flight pushes before flushing the stream" —
`'end'` fires while a 40 ms push is still pending; without the chain
the iterator returns done before the chunk's events queue.
Bisected: both tests fail without the chain, pass with it. E2E re-smoke
(`bun src/index.ts hetero exec --type claude-code` simple text + tool-using
prompt + stdin) still produces clean ordered JSONL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 refactor: remove dead Search Summary chain
Footer.tsx in web-browsing Search portal had near-zero usage. Removing it
makes the entire chain dead: triggerAIMessage, summaryPluginContent,
fillPluginMessageContent, saveSearchResult, plus the inSearchWorkflow param
threaded through internal_execAgentRuntime.
Part of LOBE-8519 — clears the path before introducing agentDispatcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add agentDispatcher.selectRuntimeType
Centralizes the client / gateway / hetero routing decision so every entry
point shares one source of truth. parentRuntime override lets sub-agent
dispatches inherit their parent operation's runtime.
Part of LOBE-8519 — call sites are migrated in following commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route sendMessage through selectRuntimeType
Compute runtimeType once per sendMessage call and dispatch off it instead of
re-deriving the hetero/gateway/client decision inline. Behavior is identical;
this just centralizes the routing rule (LOBE-8519, A1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route regenerate / continue through selectRuntimeType
regenerateUserMessage and continueGenerationMessage in the conversation store
now consult selectRuntimeType for routing. Hetero variants of both are not yet
implemented (they currently fall through to client mode with a TODO + warning).
Also drops chatStore.continueGenerationMessage — the conversation-store version
is the only caller; the chat-store duplicate had zero production usage.
Part of LOBE-8519 (A2, B4 deletion, B5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route resume helpers through selectRuntimeType
approveToolCalling / rejectToolCalling / rejectAndContinueToolCalling now
consult selectRuntimeType (via #shouldUseGatewayResume) using the operation's
own ConversationContext, instead of the bare isGatewayModeEnabled() check.
Behavior is preserved (gateway resume vs. local resume); hetero resume is not
yet implemented and falls through to the client local path.
Part of LOBE-8519 (A3, A4, A5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route sub-agent dispatch through selectRuntimeType
directMentionRoute and callAgent now consult selectRuntimeType using the
parent agent's config so sub-agent dispatches inherit the parent runtime.
Only the client path is wired today; gateway / hetero variants warn + fall
through with TODOs for follow-up.
Part of LOBE-8519 (B3, B6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: rename internal_execAgentRuntime to executeClientAgent
Aligns the client runner's name with executeGatewayAgent and
executeHeterogeneousAgent so the three runtimes share a consistent
verb-noun pattern. Pure rename — no behavioral changes; log prefixes
and test mock variables follow the new name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(todo-progress): use colorFillSecondary so left/right borders are visible against QueueTray
The colorBorderSecondary stroke nearly vanished against the dark elevated bg, so the TODO card looked open on the sides when stacked under QueueTray. Match QueueTray's outer border token (colorFillSecondary) for a consistent visible seam; inner dividers keep colorBorderSecondary as a softer secondary level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): extract producer pipeline into shared package
LOBE-8516 phase 0. Move the JSONL framing + adapter conversion + toStreamEvent
chain out of the renderer into a new `@lobechat/heterogeneous-agents/spawn`
entry, then have desktop main run it before broadcasting. Renderer now
consumes ready-made `AgentStreamEvent`s on `heteroAgentEvent`, dropping ~50
lines of in-renderer adapter wiring.
This unifies the wire shape across desktop main, the upcoming `lh hetero exec`
CLI, and the server `heteroIngest` handler — every consumer gets the same
stamped `AgentStreamEvent` with no per-consumer adapter step.
The desktop CC flow is unchanged behavior-wise: same adapter, same persistence
ordering, same step-boundary semantics; only the seam between main and
renderer moved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): pull codex tracker into shared spawn, drop desktop's gateway-client dep
Two cleanups on top of the phase 0 refactor:
1. Move `CodexFileChangeTracker` (+ its test) out of `apps/desktop/src/main/modules/heterogeneousAgent/` into `packages/heterogeneous-agents/src/spawn/`. `AgentStreamPipeline` now auto-instantiates it when `agentType === 'codex'`, so the desktop controller (and the future `lh hetero exec` CLI) stays agent-agnostic — no more "if codex { wire tracker via transformPayload }" branching at the call site. The public `transformPayload` hook is removed since it had no other consumer.
2. Re-export `AgentStreamEvent` / `AgentStreamEventType` from `@lobechat/heterogeneous-agents/spawn` and drop `@lobechat/agent-gateway-client` from `apps/desktop/package.json`. The gateway-client package is a browser-side WebSocket client; producer-side callers (desktop main, sandbox CLI) shouldn't carry it as a direct dep — they only need the type, which now flows through the producer-side entry.
Type predicate on Codex payloads tightened to a non-`Required<>` shape so the moved file passes the root tsconfig's `strict: true` (apps/desktop's tsconfig was lax).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧑💻 chore(local-testing): harden electron-dev.sh process management
Lifecycle improvements for the local-testing helper so smoke runs against the desktop dev session are reliable:
- `find_project_pids` now also catches user-started `bun run dev` Electron sessions (matches by project electron path, not just `--remote-debugging-port`), the launcher subshell saved to PIDFILE, and any process bound to the CDP port. Vite match tightened to `electron-vite[/.].*\bdev\b` so unrelated Vite invocations aren't swept up.
- `do_stop` expands seed PIDs into their descendant trees (DFS via `pgrep -P`), SIGTERMs the whole tree, waits 5s, then SIGKILLs survivors. Belt-and-suspenders sweep for stragglers + anything still bound to the CDP port. Closes the long-standing "Helper processes survive the kill" gotcha.
- `do_start` detects existing project Electron/vite before tearing it down so the user sees what's being killed; waits for port + user-data-dir locks to release before relaunching to avoid the "user data directory in use" race.
- `wait_for_cdp` uses an explicit deadline + early bail-out if the launcher PID dies, instead of the previous fixed-step loop. `wait_for_renderer` no longer pre-sleeps 10s.
`setsid` use is intentional; it puts the launched Electron in its own session so the whole tree shares a PGID we can signal in one shot. Note: `setsid` is GNU coreutils — on macOS without `brew install util-linux` the script will fail at the launch step. Documented as a known limitation; no fallback added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): gate session-complete on stdout fully drained
Node may emit `proc.on('exit')` BEFORE child stdio fully closes (documented
in child_process: "stdio streams might still be open"). Phase 0 of LOBE-8516
moved adapter ownership to main, so renderer no longer flushes its own
adapter on session-complete — meaning trailing events synthesized by
`pipeline.flush()` (e.g. Codex's `tool_end` for unfinished tool calls) would
race against, and lose to, the `heteroAgentSessionComplete` broadcast,
leaving renderer-side persistence to finalize on incomplete state.
Fix: in `proc.on('exit')`, await `streamFinished(stdout)` (covers `'end'`,
`'close'`, and `'error'`) BEFORE awaiting the broadcast queue. The first
await ensures the `stdout.on('end')` handler has had a chance to schedule
`pipeline.flush()` onto the queue; the second drains it. Only then do we
broadcast complete / error.
Regression test repros the documented Node race by emitting `exit` before
`stdout.end()` and asserts every `heteroAgentEvent` (including the
synthesized `tool_end` from `pipeline.flush()`) lands before
`heteroAgentSessionComplete`. Bisected: test fails without the gate, passes
with it.
Also: add `packages/heterogeneous-agents` to `apps/desktop/pnpm-workspace.yaml`
to mirror the new workspace dep added in the phase 0 refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): drop builtin-tool-claude-code dep, inline the 3 CC wire shapes the adapter needs
Phase 0 added `@lobechat/heterogeneous-agents` as a runtime dep of the desktop
main process. That transitively pulled in `@lobechat/builtin-tool-claude-code`
(declared in the shared package's deps), which the desktop pnpm workspace
doesn't list — CI install on the desktop project fails:
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND In ../../packages/heterogeneous-agents:
"@lobechat/builtin-tool-claude-code@workspace:*" is in the dependencies but
no package named "@lobechat/builtin-tool-claude-code" is present in the
workspace
The dep is also a layer-violation: `heterogeneous-agents` is the producer
side (CLI stream → AgentStreamEvent), `builtin-tool-claude-code` is the UI
tool definition (renderers / inspectors / agent template). Producer
shouldn't depend on UI-tool packages, even if today the import is just
types/constants — the dep cascade still drags `shared-tool-ui` etc. into
every workspace that wants the adapter.
Fix: inline the three things the adapter actually uses (`'TodoWrite'` tool
name string, `TodoWriteArgs` interface, `ClaudeCodeTodoItem` interface).
They reflect upstream Claude Code's wire schema — if `claude` ever renames
`TodoWrite`, the adapter and the downstream renderers must both update
regardless of whether they share a constant. Renderer-side packages
(`builtin-tools/codex/TodoListRender`, etc.) keep importing the canonical
`ClaudeCodeApiName` from `@lobechat/builtin-tool-claude-code`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(local-system,cloud-sandbox): drop "Local" prefix from tool names
LLM-facing tool names dropped the redundant "Local" / "LocalFiles" prefix
to shrink manifest/system-prompt token footprint:
editLocalFile→editFile, globLocalFiles→globFiles, listLocalFiles→listFiles,
moveLocalFiles→moveFiles, readLocalFile→readFile,
searchLocalFiles→searchFiles, writeLocalFile→writeFile.
Also removed `renameLocalFile` entirely from the new surface — `moveFiles`
already covers in-place renames by changing only the filename in newPath.
Old long names are still recognised in the rendering path
(client Render/Inspector/Intervention/Streaming registries, placeholders,
workflow display labels, i18n keys) and in Gateway/CLI routing, so
historical messages and older Gateway versions keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(local-system): reuse LocalSystemApiName / LocalSystemIdentifier exports
Drop the inline LOCAL_SYSTEM_IDENTIFIER / READ_FILE / LIST_FILES consts in
the snapshot materializer and import the canonical values from the package.
Mark LocalSystemApiName `as const` (matching CloudSandboxApiName) so values
narrow to literal types and satisfy LocalSystemToolSnapshot.apiName.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(builtin-tools): retire lobe-tools alias and slim lobe-notebook to render-only
- Drop the deprecated `'lobe-tools'` identifier alias from the inspector / render
registries plus its backward-compat checks in dbMessage selectors and the dev
RenderGallery fixtures.
- Hoist the only surviving notebook UI (the `createDocument` document card) into
`packages/builtin-tools/src/notebook/`, mirroring the github tool layout.
Marked the new module `@deprecated` with a ~3-month removal target.
- Delete `packages/builtin-tool-notebook/src/client/` entirely and unregister
notebook from the inspectors / interventions / placeholders / streamings
registries (it can no longer be invoked by the LLM, so those surfaces are dead
code). Manifest / executor / ExecutionRuntime stay so legacy tool calls keep
resolving.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔧 chore(builtin-tools): drop redundant antd peer dep
antd is already provided by the workspace and peered through
@lobehub/ui, so listing it explicitly on builtin-tools is noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tools): add UI render for github marketplace tool
Register an Inspector + Render for the marketplace `github` MCP tool
(single `run_command` API that wraps the `gh` CLI). Mirrors the codex
pattern under packages/builtin-tools/src/github/.
- Inspector: GitHub brand chip with the parsed gh subcommand and a
success/error indicator after the call resolves.
- Render: ToolResultCard with the full gh command (sh-highlighted) and
the output, auto-detected as JSON for `gh api` / `--json` calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tools): add inspector renders for moveLocalFiles and exportFile
Cloud-sandbox and local-system both expose moveLocalFiles, and cloud-sandbox additionally
exports exportFile, but none of these had inspector components registered, so the title
area in tool calls fell back to the default loading text. Add a shared
createMoveLocalFilesInspector factory and a cloud-only ExportFileInspector, then wire them
into both packages' inspector registries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): drop redundant "GitHub:" prefix in github inspector
The chip already shows the GitHub icon and a `gh` prefix next to the subcommand,
so the leading "GitHub:" text was duplicating that signal. Always render the chip
(even when no subcommand has streamed yet) and remove the now-stale margin and
streaming-only branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): hoist gh prefix out of github inspector chip
Move the literal `gh` text to plain leading copy with the GitHub icon as a separator,
and let the chip carry only the gh subcommand (e.g. `api /repos/...` or `search code ...`).
Reads more like the actual command and lets the verb stand out as the chip's first token.
Also seed a github run_command fixture in /devtools so the chip layout is preview-able.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): hoist github icon out of chip too
Move the GitHub icon next to the literal `gh` prefix so the chip carries only the
gh subcommand (api /repos/..., search code ..., etc.). Reads as: [icon] gh [chip].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(server,task): batch run subtasks in dependency order
Adds a "Run all" entry on the subtasks panel that kicks off the first
dependency layer; subsequent layers fire automatically as upstream tasks
complete. Layer planning (Kahn topo sort + cycle detection) lives in a
new TaskGraphService and runs server-side via two TRPC procedures.
Also fixes a pre-existing bug where `task.updateStatus(completed)` was
flipping unlocked dependents to `running` without ever invoking the
runner — leaving them in a phantom running state with no topic in
flight. Cascade now goes through TaskRunnerService.cascadeOnCompletion
from all three completion paths (TRPC updateStatus, brief approval,
judge auto-pass), so dependency chains advance end-to-end on their own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(server,task): preserve edges to in-flight and out-of-scope upstreams
The graph used to drop any dependency edge whose upstream wasn't in the
runnable set. That silently freed two correctness-breaking cases:
- A backlog subtask that depends on a *running / scheduled* sibling
landed in layer 1 and got kicked off before its blocker finished.
- A descendant that depends on a task *outside the current subtree*
(allowed by the schema) lost its blocker entirely and ran prematurely.
Edges are now classified per dependency: terminal-OK upstreams drop the
edge; in-batch runnable upstreams keep their in-degree contribution; any
other status — in-flight, runnable but out of scope, or unknown — marks
the dependent as `blockedExternally` and excludes it from the layered
plan. External blockage propagates transitively through in-batch edges
so we never run a downstream of a blocked task either. `planForParent`
fetches statuses for cross-scope upstreams so the classifier has real
data to decide on.
The UI surfaces the new bucket via `RunSubtasksPreview` and keeps the
modal open (with the run button disabled) when a plan has nothing to
start but does have blocked tasks worth explaining.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): complete local op on auth_failed to unstick input loading
When the gateway client receives `auth_failed` (server has GC'd the op or
the refreshed JWT no longer matches), the local op stayed `running`
forever — input kept the stop button, and `topic.metadata.runningOperation`
never cleared, so every revisit re-fired the same broken reconnect.
Treat `auth_failed` as session-terminal alongside `session_complete` so
`onSessionComplete` fires and `completeOperation` runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(gateway): support recoverable auth_expired with token refresh
When the JWT expires while the operation is still alive on the server,
sending `auth_failed` is wrong — the op is fine, only the credential
went stale. Treat that as a separate, recoverable signal instead.
Server (agent-gateway repo) emits a new `auth_expired` message and
keeps the WebSocket open. The client refreshes its JWT (via the
existing `aiAgentService.refreshGatewayToken`), updates the in-flight
client, and reconnects. `auth_failed` stays terminal for cases where
the op truly no longer exists.
Mirrors the device-gateway-client pattern (`auth_expired` event +
`updateToken` + `reconnect`). If no `tokenRefresher` is wired in (or
the refresh itself fails), we fall back to terminal so the input
doesn't stay stuck on the loading state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): disconnect ws on auth_expired without tokenRefresher
The server keeps the WebSocket open after `auth_expired` (so the client
can refresh and re-auth on the same connection). When no `tokenRefresher`
is wired in, we mark the local op complete but were leaving the socket —
heartbeat and autoReconnect kept running indefinitely after the op was
gone, leaking background connections.
Mirror the refresh-failure branch and call `client.disconnect()` before
firing onSessionComplete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): make tokenRefresher required on connectToGateway
Both real callers (executeGatewayAgent + reconnectToGatewayOperation)
already supply a refresher built from `aiAgentService.refreshGatewayToken`,
and there's no scenario where a Gateway op runs without a topic to refresh
against. The optional path was carrying its own foot-gun (socket leak if
forgotten) and a defensive ternary on `result.topicId` that the type
already rules out.
Required-only collapses both into the existing refresh-failure branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): inline token refresh, take topicId instead of refresher
Both callers of connectToGateway built identical refresher closures over
`aiAgentService.refreshGatewayToken(topicId)`. Pass `topicId` directly and
let connectToGateway call the service inline — gateway.ts already imports
aiAgentService for the cancel-handler path, so no new coupling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 chore(gateway): rewrite stale auth_expired comment
The "no refresher provided" branch is gone — fold that case out of the
comment and explain why the catch branch needs explicit disconnect().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): add max(50) pagination cap to file.recentFiles and file.recentPages
Fixes GHSA-jr3g-w7rp-fhm9: unbounded limit parameter in recentFiles
and recentPages endpoints allowed authenticated users to trigger
arbitrarily large DB queries (amplified 3x before the DB call).
Adds .max(50) Zod constraint to cap both endpoints at 50 items.
* fix(security): add pagination caps to topic.getTopics, rankTopics, recentTopics
Fixes GHSA-jr3g-w7rp-fhm9:
- getTopics.pageSize: .max(100)
- rankTopics input: .max(50)
- recentTopics.limit: .max(50)
* fix(security): add pagination caps to session.getSessions and rankSessions
Fixes GHSA-jr3g-w7rp-fhm9:
- getSessions.pageSize: .max(100)
- rankSessions input: .max(50) (multi-JOIN aggregate query)
* fix(security): add max(100) pagination cap to agent.queryAgents
Fixes GHSA-jr3g-w7rp-fhm9: unbounded limit parameter in queryAgents
allowed resource exhaustion via arbitrarily large DB queries.
* fix(security): add max(100) pagination cap to document.queryDocuments
Fixes GHSA-jr3g-w7rp-fhm9: unbounded pageSize parameter in queryDocuments
allowed resource exhaustion via arbitrarily large DB queries.
* 🐛 test(web-crawler): remove zhihu test cases after rule removal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tool-task): add Inspector + Render, batch createTasks/runTasks
Adds chip-style Inspector and per-API Render to the lobe-task tool, plus two
batch APIs (createTasks, runTasks) so an agent can plan or launch a set of
subtasks in a single call instead of calling createTask/runTask N times.
runTask/runTasks call taskService.run, actually triggering TaskRunnerService
and producing a topic+operationId — distinct from updateTaskStatus(running),
which only flips a flag. The system prompt now spells this out so the model
stops conflating the two. Already-running, missing-assignee, and per-item
failures surface back to the agent with clear messages.
Fixes LOBE-8438
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(server,task): implement createTasks/runTask/runTasks in server runtime
The manifest exposes these APIs to the model, but only the client-side
executor was implemented. Server-side tool execution (src/server/services/
toolExecution/builtin.ts) throws "Builtin tool ... is not implemented" when
the runtime is missing a method, so production paths that route through the
server runtime would fail at runtime.
- Extracted createTaskImpl as a reusable closure so createTasks loops can
reuse the parent-resolution + assignee-validation flow without copy-paste
- runTask / runTasks call taskCaller.run(...) which already routes to
TaskRunnerService — same execution path as the UI/CLI run buttons
- runTasks continues past per-item failures and reports them in the summary
(matching the client executor's behavior)
- Added 7 tests (20 total in this file) covering happy path, per-item
failure, missing identifier, and current-task fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task-drawer): hide topic feedback input until run terminates
Feedback can only steer the next run, so showing the input while the
topic is pending/running was misleading — gate it on terminal status
(completed/failed/canceled/timeout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 fix(builtin-tool-agent-documents): wire Inspectors into registry, switch to chip UI
The Inspector components for lobe-agent-documents existed but were never
registered in packages/builtin-tools/src/inspectors.ts, so the chat UI fell
back to the default "(id:316c6ad5-10e7-46ff-8ccf-15f2359c19...)" header
that shows raw param dumps. Registering them is the root fix.
While in there, refactored all 9 inspectors to the chip pattern used by the
other builtin tools — full UUIDs are noisy in a one-line header, so document
ids are truncated to their first 8 chars (prefixed ids like agd_… are left
intact since they're already short). Each inspector now surfaces the most
useful per-API context: title chip when known (Read/Create), id chip + new
title (Rename/Copy), op count + success ratio (Modify), char count
(Replace), target scope + doc count (List), rule type (UpdateLoadRule),
red dashed line-through (Remove). Shared chip styles live in one
_styles.ts so the visual language stays consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 📝 docs(.agents/skills): add builtin-tool skill
Self-contained reference for building/extending lobe-* builtin tools —
SKILL.md entry point plus architecture / tool-design / ui deep-dives.
Sits alongside the other agent skills.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix: sync DEFAULT_MODEL/DEFAULT_MINI_MODEL into desktop business-const stub
#14379 moved DEFAULT_MODEL and DEFAULT_MINI_MODEL into @lobechat/business-const,
but the desktop workspace stub at apps/desktop/stubs/business-const wasn't
updated, breaking the desktop client build with MISSING_EXPORT errors.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add feedback input at bottom of TopicChatDrawer (LOBE-8441)
Mount a comment box inside the Topic Run drawer so users can leave
feedback and trigger a follow-up topic run without leaving the drawer.
Send button calls addComment then runTask (without continueTopicId, so
a brand-new topic is started instead of resurrecting the completed one).
Existing AgentTaskDetail/CommentInput is untouched — the new component
lives next to TopicChatDrawer and stays separate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: close TopicChatDrawer after submitting feedback
Closing the drawer once the comment is persisted and the new run is
kicked off matches user expectation — leaving it open made it look
like the existing topic was the one being run again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(document-modal): show skeleton for title while document is loading
Replace the "Untitled" placeholder and AutoSaveHint with a skeleton in both the modal header and the in-page title editor while the document is still being fetched, so the empty fallback no longer flashes before content arrives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-detail): add run-now dropdown next to cancel-schedule button
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-artifacts): show created time and sort newest first
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(release-template): drop Highlights from db-migration changelog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(release-template): drop version numbers from changelog templates
Patch releases auto-bump on merge, so the version isn't known when the
changelog is authored. Replace `# 🚀 LobeHub v<x.y.z> (YYYYMMDD)` with
`# 🚀 LobeHub Release (YYYYMMDD)` in all changelog examples and the
GitHub Release Changelog Template inside SKILL.md, and replace the
hard-coded `Since v...` / `Full Changelog: v...v...` lines in the
weekly-release example with the same `<previous-tag>` placeholder
already used by the SKILL.md template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Automatic sync from main to canary. Merge conflicts detected.
**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260501-25207007930
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.
Keep canary-side logic in useSend (active home agent), feedback action
planner procedure-state, useSend test mocks, and e2e Home chat-input
step. The main-side blocks referenced removed symbols and outdated
action-planning code that would break compile/tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): always synthesize a brief on scheduled-task ticks
Heartbeat ticks remain mid-loop nudges and are still skipped, but
schedule-mode tasks now bypass both the trivial-content rule gate and
the LLM emit-vote so each scheduled run produces a daily brief.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(taskDetail): switch activity timestamps to absolute date once gap exceeds one day
Adds formatActivityTime helper to @lobechat/utils/time: relative phrasing
under 24h, localized date (e.g. "4月29日" / "Apr 29") afterwards, with the
full datetime exposed via the native title attribute on hover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): fork chainGenerateBrief prompt so scheduled ticks always produce a brief
The default prompt instructs the LLM to pair `emit=false` with an empty
title, so even after we bypassed the emit-vote for scheduled tasks the
downstream `!title || !summary` guard could still drop the brief and
silently break the "every schedule tick must produce a brief" contract.
chainGenerateBrief now takes a forceEmit flag; when true it swaps to a
scheduled-tick prompt that removes the skip branch and mandates a
non-empty title/summary, including the "no new activity today" path.
synthesizeTopicBrief passes forceEmit=true for schedule-mode tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update @google/genai version to ~1.50.1
* 💄 style(conversation): stack TodoProgress + QueueTray as a floating overlay above ChatInput
Move TodoProgress out of normal flow and render it together with QueueTray
inside ChatInput as a single absolute-positioned overlay anchored to the
input's top edge. The overlay no longer pushes ChatList up; instead it sits
as a "cover layer" above the scroll viewport.
To keep chat content reachable above the overlay, expose the overlay's
measured height via the conversation input store (ResizeObserver in
ChatInput) and have VList consume it as `paddingBottom = max(24, height +
12)` — the +12 compensates for ChatInput's `marginTop: -12`. BackBottom
also reads the same height via a new `bottomOffset` prop so the
back-to-bottom button lifts above the overlay instead of being occluded.
QueueTray sits on top, TodoProgress below; TodoProgress squares its top
corners (`topAttached`) when QueueTray is present so the two panels fuse
into a clean stack with no notches at the seams.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(utils): make formatActivityTime title assertion timezone-independent
The test hardcoded `2026-05-01 13:00:00` (UTC+8 author tz), so it failed in
UTC CI as `2026-05-01 05:00:00`. Derive the expected title via the same
dayjs format the implementation uses so the assertion holds regardless of
the runner's timezone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(conversation): include chatInputOverlayHeight in store mock state
The store always initializes chatInputOverlayHeight to 0 via inputInitialState,
so the State type rightly keeps it required. The selectors test mock simply
missed the field after the slice gained it; supply 0 to match the real
initial state instead of weakening the type to optional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(brief): split judge from generate, persist decision on task topic
Split the brief-emission flow into two independent stages so judgment and
copy-generation are no longer entangled in a single LLM call (which made
the scheduled-tick fork necessary in the first place).
- Rule layer (`shouldEmitTopicBrief`) goes three-state: `'yes' | 'no' |
'unknown'`. Conclusive cases (error / review-handled / review-configured
/ heartbeat / trivial-non-scheduled / scheduled) bypass the LLM entirely;
only manual + non-trivial topics fall through to `'unknown'`.
- New `chainJudgeBriefEmit` (small chain, returns `{emit, reason}`) is
invoked ONLY on the `'unknown'` branch. Title/summary copy is no longer
in scope for this call.
- `chainGenerateBrief` drops the `forceEmit` fork and the `emit` field —
it now assumes the caller has already decided to emit and just produces
`{title, summary}`. Saves tokens on skip paths since we never draft copy
for a brief that won't be persisted.
- Every decision (rule or LLM) is persisted to
`taskTopics.handoff.briefDecision` via a new `updateBriefDecision` model
method using `jsonb_set + COALESCE` so existing handoff fields aren't
disturbed. Gives operators a per-topic audit trail of why a brief was
or wasn't produced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(brief): emit on errors, defer heartbeat to LLM judge
Two follow-up tweaks to the rule layer (`shouldEmitTopicBrief`):
- `reason === 'error'` is no longer a hard skip — the user must be told the
run failed. Returns `{emit: 'yes', reason: 'execution-error'}` so once
the error path is folded into `synthesizeTopicBrief` (separate
consolidation refactor) the verdict is correct without further changes.
Currently dead code: `onTopicComplete` still builds an urgent error
brief inline at the `else if (reason === 'error')` branch.
- Heartbeat ticks change from a hard `'no'` to `'unknown'`. Most ticks are
mid-loop noise but the occasional one warrants surfacing, and only the
LLM can read the content to tell. Heartbeat is at minimum 10 min so the
added judge call per tick is acceptable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix(skill): skip OAuth redirectUri on desktop to prevent broken app:// navigation
On desktop (Electron), window.location.origin is app://renderer which the system browser cannot navigate to. Skip passing redirectUri so market shows a default success page instead, relying on existing window-close monitoring and fallback polling to detect OAuth completion.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): tokenize mdfind keywords, scope glob to home, align tool prompts
- mdfind treats free-form keywords as a single literal substring; "LobeHub
Financial Statement" never matches "Financial_Statement_LobeHub.pdf".
Split on whitespace and AND each token (still substring-matched) so
ordering doesn't matter.
- Unix/Windows glob fell back to process.cwd() — meaningless inside a
packaged Electron app. Default to os.homedir() instead so unscoped
patterns can actually find user files.
- systemRole/systemRole.desktop documented `query`/`onlyIn`/`path` for
searchLocalFiles/grepContent/globLocalFiles, but the manifest exposes
`keywords`/`scope`. The wrong names were silently dropped, so the LLM
could never scope its searches. Aligned the prompts with manifest and
noted the new keyword-tokenization semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): preserve glob/grep error in tool message content + tidy file row UI
Two independent bugs that combined to break Glob/Grep tool messages and
then made search hits look ugly in the result list.
Empty `content` on glob failure
- LocalSystemExecutionRuntime.normalizeResult dropped `raw.error` when
mapping `globLocalFiles`/`grepContent`, so a failure from the IPC layer
(e.g. fast-glob throwing EACCES while traversing the wrong cwd) became
`{ result: {...}, success: false }` with no error attached.
- ComputerRuntime.errorOutput then did
`result.error?.message || JSON.stringify(result.error)`. With error
undefined that yields the value `undefined` (not the string), which
collapsed into `content: ""` downstream — the chat store still saved
`pluginState` so users saw a tool message with state set but the
Response panel completely blank.
- Propagate `raw.error` through normalizeResult and harden errorOutput
with a "Tool execution failed" fallback so the LLM and the debug panel
always get a real string.
Search results layout
- FileItem stacked filename and a redundant full path on a single
baseline-aligned row, so the path column repeated the filename and
felt visually off-balance.
- Switch to a two-line layout: filename on top, parent directory only
(collapsed via displayRelativePath when available) underneath, both
vertically centered against the file icon.
- Promote the "open containing folder" action from hover-only to a
permanent right-side button so it's reachable in one click.
- Bump the SearchFiles scroll container so the taller rows still show a
reasonable number of hits before scrolling, and add a Downloads-style
fixture to the dev panel render gallery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): harden executor toResult to never emit empty content and to keep state on failure
The earlier fix patched normalizeResult and ComputerRuntime.errorOutput,
but the central funnel where every executor return is shaped —
LocalSystemExecutor.toResult — still trusted the runtime output blindly:
- the success=false branch dropped `state` entirely, which meant any
partial pluginState a runtime had built up was thrown away the moment
it reported an error (renderers then re-rendered as if the call had
produced nothing).
- both branches passed `output.content` through verbatim, so an
upstream regression that forgot to populate content (the recent Glob
EACCES path) would still surface as a blank Response panel.
Make toResult the strict gate it claims to be: derive a non-empty
content from `output.content -> output.error.message -> "Tool execution
failed"`, and always propagate `state` regardless of `success`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 chore(devtools): sanitize searchLocalFiles fixture to use synthetic data
Replace real-looking filenames, paths and corporate identifiers in the
RenderGallery fixture with neutral sample-user / sample-quarterly-report
placeholders. The fixture is checked into the repo and shipped to every
contributor's dev panel — it shouldn't carry data that resembles a
specific person's Downloads/iMessage/WeChat layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): keep recurring tasks active when resolving their result briefs
Approving a `result` brief on a recurring (`automationMode='schedule'`)
task was flipping the parent task to `completed`, which removed it from
the active board and stopped future scheduled runs from surfacing on it.
A daily brief is one occurrence — accepting it is a UI dismissal, not a
lifecycle terminal.
The discriminator is the **task's** automation mode, not the brief's
`cronJobId`. A manual run of a recurring task has `cronJobId=null` but
the task is still recurring, so a cronJobId-based check would let that
case slip through.
- Server: `BriefService.resolve` now loads the task and only completes
it when `automationMode !== 'schedule'`.
- Server: `enrichBriefsWithAgents` also batches the task lookup and
exposes `taskAutomationMode` on the listed briefs so the UI can label
the action correctly without an extra round-trip.
- UI: the result action label switches to "Mark as resolved" /
"标记为已解决" when `taskAutomationMode === 'schedule'`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): unify result brief action to "Confirm" and key off task status
Replace the dual confirmDone/markResolved labels with a single brief.action.confirm,
and gate task completion on task.status !== 'scheduled' so heartbeat-mode tasks
parked between ticks are also kept active when one of their result briefs is
approved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): restore "Confirm complete" for terminal-accept; "Confirm" only for status='scheduled'
Bring back brief.action.confirmDone alongside the new brief.action.confirm.
The dual-label discriminator is the parent task's runtime status: tasks parked
at 'scheduled' show "Confirm" (dismiss-only — server keeps them active for the
next tick), all other states show "Confirm complete" since approving will flip
the task to completed. Server keeps its task.status !== 'scheduled' guard.
Threads taskStatus on BriefItem / BriefWithAgents (replacing the previously
removed taskAutomationMode) so the UI label matches the actual server effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): make BriefItem.taskStatus optional for locally-constructed briefs
TaskActivities.tsx builds a BriefItem from a TaskDetailActivity row and has no
task-status info to pass through. Marking the field optional matches the prop
shape on BriefCardActions and lets the activity feed compile again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(followUpAction): add shared types and JSON schema for follow-up chip extraction
* 🐛 fix(followUpAction): tighten JSON schema literal types with top-level as const
* ✨ feat(followUpAction): add base + onboarding prompt builders
* ✨ feat(followUpAction): add server service to extract chips via fast LLM
* 🐛 fix(followUpAction): drop empty chips and consolidate schemas in schema.ts
* ✨ feat(followUpAction): expose extract via lambda TRPC router
* ✨ feat(followUpAction): add client service wrapper around TRPC mutation
* ✨ feat(followUpAction): add zustand store with abort/timeout actions
* 🐛 fix(followUpAction): stabilize empty selector ref and abort on reset
* ✨ feat(followUpAction): add FollowUpChips component with reply icon style
* ✨ feat(followUpAction): add onboarding glue hook with phase/greeting guards
* ✨ feat(followUpAction): wire chips + glue hook into onboarding conversation
* 🐛 fix(followUpAction): drop unused eslint-disable directive in client service
* 🐛 fix(followUpAction): tighten types and align prompt with schema bounds
* 🐛 fix(followUpAction): use fresh phase for chip extraction across phase boundaries
* 🐛 fix(followUpAction): type SUGGESTION_RESPONSE_JSON_SCHEMA against GenerateObjectSchema
The earlier `as const` widened to readonly literal types, which is incompatible
with the mutable `GenerateObjectSchema` interface required by `generateObject`.
Replace with an explicit type annotation so the literal is checked at definition
and stays assignable at the call site.
* ⚡️ perf(followUpAction): only refresh user/agent caches at onboarding phase boundaries
The previous logic refreshed both useUserStore and the webOnboarding builtin
agent after every assistant turn, but their content only changes when the
phase advances or onboarding finishes. Compare prev vs next phase/finishedAt
from syncOnboardingContext and skip the two refresh calls when neither moved,
saving an RPC per intra-phase turn.
* 🐛 fix(followUpAction): read finishedAt from agentOnboarding subobject
* ♻️ refactor(followUpAction): take agentId from caller and resolve model from agent config
Drops the env-var override path on the server. The service is meant to be
generic across consumers, so the caller now passes the agentId of the
conversation context. The service resolves model/provider from
AgentModel.getAgentConfigById, falling back to DEFAULT_SYSTEM_AGENT_CONFIG.topic
when the agent has no explicit model. The onboarding caller passes the
webOnboarding builtin agent id; future consumers pass theirs.
* 🐛 fix(followUpAction): resolve latest text assistant message server-side via topicId
* ✨ feat(followUpAction): mirror assistant language and ban deferral chips
Two prompt rule changes:
1. Match the assistant message's language instead of forcing English. The
chip should be in the script the user would naturally reply in.
2. Prefer questions with explicit options when the message contains
several, and ban "Let me think / Skip / You decide / Let me explain"
style escape-hatch chips entirely. Every chip must be a concrete
reply the user might actually send; the user can always type
freely, so meta deferral chips just waste a slot.
* 🐛 fix(followUpAction): bump timeout to 20s and silence TRPC-wrapped abort
The previous 3s timeout aborted the LLM call before generateObject could
respond — a typical extract round-trip is ~10s. Bump to 20s.
Also silence the TRPCClientError that wraps the abort: TRPC re-throws
DOMException as TRPCClientError("signal is aborted ..."), so the
original `instanceof DOMException` check missed it and noise
`[FollowUpAction] extract failed` warnings hit the console on every
manual clear / new turn. Now we also short-circuit on `signal.aborted`.
* feat: enhance chat input functionality with new flags
- Added `disableMention` and `disableSlash` props to `ChatInput` and `StoreUpdater` to control mention and slash command triggers.
- Introduced `disableFollowUpVariant` and `disableQueue` props to manage placeholder behavior and message queuing during agent streaming.
- Updated `FollowUpChips` to handle topic IDs and prevent rendering during message generation.
- Refactored onboarding context retrieval to streamline fetching of user persona and state.
- Removed deprecated onboarding state API references and adjusted related tests.
- Improved follow-up action handling to discard stale results based on active request controllers.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat: enhance agent marketplace onboarding with summaries and improved state management
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 💄 style(brief): use Footprints icon and hide view-run until card hover
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): swap icon to Workflow for the View run shortcut
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): forward all search params and guard empty mdfind
- Pass through all resolved searchFiles params (keywords, fileTypes,
date range, scope, etc.) instead of dropping everything except
`directory`, which previously caused the executor to call mdfind
with no query.
- Surface missing fields (`keywords`, `fileTypes`, `contentContains`,
date range, sort, etc.) on `SearchFilesParams` so the cross-runtime
type matches the actual contract.
- Short-circuit Spotlight search when there is no query expression so
mdfind doesn't print its usage text and get parsed as phantom file
hits, and drop unstattable rows instead of fabricating 0-byte
placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(skills): guard empty command and forward description in desktop execScript
Desktop skills' execScript dropped `description` before IPC, so when an LLM tool call arrived without `command` (aborted stream, empty args, etc.) the runner crashed on `command.slice(0, 50)` and surfaced as "Failed to execute command: ...".
- runner.ts: return a proper error result when `command` is missing instead of throwing
- lobe-skills.desktop.ts: forward `options.description` to localFileService.runCommand for better logs and as a fallback when command is absent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(local-system): show empty state when file search returns no results
Previously the SearchFiles result panel rendered an empty Flexbox when there were 0 hits, leaving the area visually blank below "Number of searches: 0". Reuse the same Block + Empty pattern as web-browsing search and the existing `search.emptyResult` i18n key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-file-shell): expand leading ~ in file operation paths
Node fs APIs don't expand `~` like a shell would, so paths supplied by
the LLM or pasted by users were failing with ENOENT. Apply expandTilde
across read/write/edit/move/rename/list/glob/grep/search and the desktop
search controller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(local-system): show empty state when listed directory has no files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: inject skill instruction into tool system role
Consume the `instruction` field from market SDK's `listTools` response
and pass it as `systemRole` on the tool manifest, so the LLM receives
skill-level guidance documentation via `<tool.instructions>` in the
system prompt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: update market-sdk
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): open run topic drawer from daily brief card
Adds a "View run" shortcut to the brief card's actions row that opens
the corresponding topic chat drawer in place on the home page, so the
user can inspect the agent's actual run without navigating to the task
detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🌐 i18n(brief): refine zh-CN copy for view run action
"查看执行" was ambiguous (could read as "execute"); use "查看运行轨迹"
to make it clear the action opens the agent's actual run trace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⚡️ perf(agent,working-sidebar): cut Review tab open latency ~9× on large dirty trees
Two changes that together drop "open Review tab" from ~1.7s to ~190ms on a
working tree with 200+ dirty files:
- GitCtr.getGitWorkingTreePatches: replace N-parallel `git diff` subprocesses
with one bulk `git diff HEAD --` for tracked files (split per-file in JS) and
direct `fs.readFile` synthesis for untracked. Eliminates the main-process
fork storm and `.git/index` lock contention. IPC drops 635ms → ~160ms.
- Review/index.tsx: replace default-expand-all with a size budget
(≤100KB cumulative patch OR 50 files). Caps Shiki tokenizer cost on first
paint and removes the 1064ms renderer freeze; small-diff workflows still
get 50 panels open, big-refactor workflows clamp to 2–3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent,working-sidebar): handle special-char paths and bulk diff overflow
Address two P2 review issues on the perf refactor (#14338):
- Quote untracked paths in synthetic diff headers. Direct interpolation of
entry.filePath into `diff --git` / `+++` lines emitted malformed headers
for filenames containing TAB / LF / CR / quote / backslash, causing the
patch parser to choke (e.g. TAB-containing names triggered "bad git-diff -
inconsistent new filename"). New quoteGitPath mirrors git's own
quote_c_style: prefix lives inside the quotes, control bytes get octal
escapes. Plain ASCII spaces stay unquoted to match git's output.
- Replace fixed-buffer bulk diff with streamed spawn + per-file fallback.
The 64 MB execFile maxBuffer would reject the entire bulk diff on
overflow, leaving every tracked file as an empty placeholder. Now bulk
output streams via spawn (no ceiling), salvages partialStdout on failure,
and routes any uncovered tracked entry through fetchTrackedPatchPerFile
with concurrency 8 — restoring the per-file truncation/binary handling
the original implementation had.
Adds GitCtr.test.ts covering quote/dequote round-trips for the problem
characters the reviewer called out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): show artifacts in card and extract DocumentModal
Wire `brief.artifacts` (already populated by topic-brief synthesis) into
TaskBriefCard and the home BriefCard so completed-topic deliverables
show up inline; clicking a doc card opens it in a modal.
The per-task PageModal becomes a reusable `DocumentModal` (props-based:
documentId/open/onClose), and the preview trigger state moves from task
store to a new `preview` slice in document store — any surface can now
call `useDocumentStore.openDocumentPreview(id)`.
Also:
- PageAgentPanelOverrideProvider: ephemeral right-panel state for
PageEditor in transient surfaces (modal); defaults collapsed and
doesn't write the persisted global preference.
- PageEditor.fullWidthHeader: layout flag so the modal's header spans
both columns instead of the left pane only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(shared-tool-ui): unify label-to-content spacing in file inspectors
Replace trailing-space spacing with explicit 6px marginInlineEnd on the label
span in Read/Edit/Write/List inspectors so they match the 6px gap already used
by chip-based renderers (Bash, Grep, Glob).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): clear preview state on document modal teardown
`previewDocumentId` is global (`useDocumentStore`) and the modal opens on
any truthy value. Without cleanup, navigating away with the modal open
left a stale id behind, and the next surface that mounted a preview
modal (e.g. /home daily brief) would immediately reopen the old doc.
Extract a `<DocumentPreviewModal />` connector that resets the preview
state on unmount, and use it everywhere the global preview should be
rendered (TaskDetailPage, DailyBrief). Future mount points get the
cleanup for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): coerce globalExpand to boolean in panel control hook
`systemStatusSelectors.showPageAgentPanel` returns `boolean | undefined`
(zenMode short-circuit ANDs with an optional flag), but
`PageAgentPanelControl.expand` is `boolean`. Coerce with `!!` so the
non-override branch satisfies the type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✨ feat(mobile-router): add task and brief routers to mobile tRPC router
Expose task and brief endpoints to the mobile client so the React Native
app can manage tasks and daily briefs via the same tRPC contract used by
the web client.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Automatic sync from main to canary. Merge conflicts detected.
**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260429-25113686179
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.
* 🐛 fix(model-runtime): preserve LLM finishReason through callbacks transformer
Soft interrupts from providers (Gemini RECITATION / MAX_TOKENS, etc.)
emit a `type: 'stop'` chunk carrying the finishReason string, but
`createCallbacksTransformer` was only using it as a terminal-event flag
and never aggregating the value. Downstream the `OnFinishData` payload
had no `finishReason` field, so RuntimeExecutors recorded an `llm_result`
event without it — the harness silently rendered an empty assistant
message even though tokens were billed.
Capture the value in the callbacks aggregator, surface it on
`OnFinishData`, and write it into the `llm_result` tracing event so
soft-interrupt cases are diagnosable.
Fixes LOBE-8403
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): keep first finishReason across multi-stop streams
Anthropic emits two `'stop'` chunks per stream — `message_delta` with
the real `stop_reason` (`end_turn` / `max_tokens` / `tool_use`) followed
by a `message_stop` sentinel. Last-write-wins clobbered the meaningful
reason with the sentinel string, defeating the very tracing signal this
fix is meant to provide.
Switch to first-non-empty-wins so the real provider reason survives.
The empty-string fallback covers cases where an early provider chunk
arrives before the reason is known.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**Hotfix Scope:** Topic preservation across cold chat-entry routes
> Keeps newly created Topics visible when a first message is sent before
the destination chat route has fully hydrated.
- **Page Agent empty-session regression** — Sending the first message in
an empty Page Agent panel no longer clears the newly created Topic and
returns the panel to an empty state. (Resolves LOBE-8351)
- **Home cold-route send regression** — Sending from the Home default
Chat Input now routes to the newly created Inbox Topic even when
`/agent/:aid` has never been opened and the route chunk has no warm
cache.
- **Page-scoped Copilot consistency** — Page Copilot and File Copilot
share the same provider-level topic reset behavior, so stale Topics are
cleared only when entering or switching the scoped Agent.
- **Regression coverage** — Added focused unit coverage for Home default
sends, route parity coverage remains intact, and added an E2E scenario
for the no-cache Home send path.
- `bunx vitest run --silent='passed-only'
'src/routes/(main)/home/features/InputArea/useSend.test.ts'
'src/spa/router/desktopRouter.sync.test.tsx'
'src/routes/(main)/agent/features/Conversation/ChatHydration/index.test.tsx'
'src/routes/(main)/agent/_layout/AgentIdSync.test.tsx'`
- `BASE_URL=http://localhost:3007
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres bun
run test -- --tags '@HOME-CHAT-COLD-001'` from `e2e/`
- Self-hosted: pull the new image and restart. No schema or environment
changes.
- Cloud: ships through the normal hotfix deployment after merge.
@Innei
Fixes LOBE-8351
* ✨ feat(creds): add local/desktop credential injection guidance
Teach AI how to use credentials in non-sandbox (desktop/local) environments via
getPlaintextCred + runCommand inline env vars, alongside the existing sandbox flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔒 fix(creds): use runCommand env param for secure credential passing
Inline secrets in the command string would be visible in the Intervention UI
and logs. Use runCommand's env parameter instead, and correct the misleading
file credential guidance (getPlaintextCred returns a fileUrl, not a local path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
✨ feat(agent,working-sidebar): add Review tab with bulk git working-tree diffs
Adds a Codex-style Review tab to the agent working sidebar (peer to the
existing Resources content, surfaced as Space). When the active topic has a
working directory bound, the sidebar shows two chip-style tabs — Space (left)
and Review (right) — and the Review pane lists every dirty file with its
unified diff rendered via PatchDiff.
A single new IPC method `git.getGitWorkingTreePatches(dirPath)` enumerates
the working tree once via `git status --porcelain -z`, then runs every
per-file `git diff` in parallel inside main; tracked entries hit
`git diff HEAD -- <file>` while pure untracked files use
`git diff --no-index /dev/null <file>`. Each patch is capped at 256 KB and
classified into added / modified / deleted with additions/deletions counts
parsed off the patch text, so the renderer needs exactly one round trip and
zero per-file fetches.
The Review pane defaults to all files expanded, with PatchDiff render gated
on the panel's expanded state so collapsed entries don't pay the shiki
highlight cost. Adds a unified/split viewMode toggle in the Review subheader,
shows an Unstaged-N chip alongside it, and ships a custom small expand caret.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
💄 style(daily-brief): add skeleton loading state for DailyBrief component
LOBE-8400
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(task): add start-scheduling button in automation popover
Lets users mark a configured task as "scheduled" without firing an
immediate run, so the cron/heartbeat tick owns the first execution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): hide start-scheduling button in heartbeat mode
Heartbeat tasks are re-armed only by maybeRearmHeartbeat after a topic
completes — there is no dispatcher that picks up `scheduled` heartbeat
tasks, so the button would leave a paused/backlog task dormant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-lifecycle): auto-synthesize topic briefs (LOBE-8333)
Replaces agent-driven createBrief on the non-review "done" path with a
programmatic synthesis: rule-based decision + DB-collected artifacts +
a dedicated LLM for user-facing title/summary. Handoff and brief stay
separate (agent-internal vs user-facing language) and the new path is
gated behind task.config.brief.mode === 'auto' so existing tasks keep
the legacy tool-driven behavior until the GrowthBook flag flips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(generate-brief): let LLM gate emission per topic content
Pure rules can only skip the obvious cases (error, judge-handled,
automation tick, trivial content). They can't tell that "I clarified
my understanding and will start drafting next" is a working note, not
a delivery. Add an `emit: boolean` to GENERATE_BRIEF_SCHEMA and have
the prompt instruct the model to judge — emit=false discards the
brief without writing to the table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task-model): move topic-artifact query into TaskModel
DB queries belong on the model, not in a service helper. Replaces
the standalone collectTopicArtifacts() with TaskModel.getDocumentsPinnedSince(),
which lives next to pinDocument / getPinnedDocuments and returns
joined { id, kind, title } rows. synthesize.ts is now pure decision
logic — no more drizzle imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: support UTF-16 encoded text files in TextLoader
The TextLoader previously hardcoded UTF-8 encoding when reading files,
causing UTF-16 encoded CSVs (e.g. Google Ads Keyword Planner exports)
to be parsed with null bytes, producing garbled content and database
insert failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor(file-loaders): tighten TextLoader UTF-16 detection
- Use TextDecoder('utf-16be') instead of manual byte-swap loop, which
also avoided in-place mutation of the read buffer.
- Replace the 2-byte heuristic with a 512-byte sample, count ASCII-pair
shape on both halves so UTF-16BE without BOM is detected too, and
files whose first character is non-ASCII no longer slip through.
- Add tests for UTF-8 BOM, UTF-16LE no-BOM, and UTF-16BE no-BOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**Hotfix Scope:** Topic preservation across cold chat-entry routes
> Keeps newly created Topics visible when a first message is sent before
the destination chat route has fully hydrated.
## 🐛 What's Fixed
- **Page Agent empty-session regression** — Sending the first message in
an empty Page Agent panel no longer clears the newly created Topic and
returns the panel to an empty state. (Resolves LOBE-8351)
- **Home cold-route send regression** — Sending from the Home default
Chat Input now routes to the newly created Inbox Topic even when
`/agent/:aid` has never been opened and the route chunk has no warm
cache.
- **Page-scoped Copilot consistency** — Page Copilot and File Copilot
share the same provider-level topic reset behavior, so stale Topics are
cleared only when entering or switching the scoped Agent.
- **Regression coverage** — Added focused unit coverage for Home default
sends, route parity coverage remains intact, and added an E2E scenario
for the no-cache Home send path.
## ✅ Verification
- `bunx vitest run --silent='passed-only'
'src/routes/(main)/home/features/InputArea/useSend.test.ts'
'src/spa/router/desktopRouter.sync.test.tsx'
'src/routes/(main)/agent/features/Conversation/ChatHydration/index.test.tsx'
'src/routes/(main)/agent/_layout/AgentIdSync.test.tsx'`
- `BASE_URL=http://localhost:3007
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres bun
run test -- --tags '@HOME-CHAT-COLD-001'` from `e2e/`
## ⚙️ Upgrade
- Self-hosted: pull the new image and restart. No schema or environment
changes.
- Cloud: ships through the normal hotfix deployment after merge.
## 👥 Owner
@Innei
Fixes LOBE-8351
* ✨ feat(agent-marketplace): implement onboarding agent marketplace picker
Adds a new builtin tool `@lobechat/builtin-tool-agent-marketplace` that
opens a categorized agent picker UI during web onboarding. The picker
fetches the live curated catalog from the marketplace API
(`/api/v1/agents/onboarding-full`) via a TRPC procedure that injects the
trust-token, and lets the user select template agents to install.
Highlights:
- Self-contained marketplace package with manifest, system role, executor,
and ExecutionRuntime
- React intervention component with category sidebar, skeleton loading
state, and avatar/empty/error UI; all user-visible strings i18n-driven
- Dependency-inverted fetcher: package exports `setAgentTemplatesFetcher`,
app registers a TRPC-backed implementation in AgentOnboardingPage
- New TRPC `market.agent.getOnboardingFull` proxies the upstream API with
trust-token authentication; client never sees secrets
- Splits the existing `saveUserQuestion` intervention into agent identity
and user profile cards for clearer onboarding approval UX
- Wires marketplace into `builtin-tools` registry, executor map, and
onboarding metrics; web-onboarding agent system prompt updated to
reference the picker
Closes LOBE-7801
* ✨ feat(onboarding): enhance early exit handling and marketplace integration in onboarding flow
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(agent-marketplace): register server runtime, scope picks per-topic, and harden onboarding handoff prompts
The summary phase silently skipped the marketplace handoff because the
server toolExecution registry had no runtime for `lobe-agent-marketplace`,
so every `showAgentMarketplace` call returned "not implemented" and the
agent fell through to `finishOnboarding`. The runtime-injected phase
guidance and action hints also instructed the agent to call
finishOnboarding directly after the summary, contradicting the new
system role.
- Register `agentMarketplaceRuntime` in
`src/server/services/toolExecution/serverRuntimes` so the executor
can actually run.
- Scope the in-memory `picks` map by `topicId` and reject a second
`showAgentMarketplace` call in the same conversation with a clear
"already opened, finish on next turn" message.
- Tighten the success content to instruct the model to STOP the current
turn after opening the picker and run closing + finishOnboarding on
the FOLLOWING user turn.
- Update `OnboardingActionHintInjector`, `PHASE_GUIDANCE.summary`,
`toolSystemRole` and `web-onboarding/systemRole` so all four prompt
layers agree: open the picker exactly once during summary, do not
call finishOnboarding in the same turn, and do not call the
submit/skip/cancel APIs ourselves.
- Stop treating short affirmations like "好的" / "行" / "ok" as
early-exit signals; they are confirmation of the summary and should
let the picker handoff proceed normally.
Verified end-to-end with `bun run agent-evals run onboarding/web-onboarding-v3
--case-id fe-intj-crud-v1 --model deepseek-v4-pro`: hard assertions all
pass, judge moves from 7/10 (premature finishOnboarding in same turn)
to 8/10 with picker opened once and finishOnboarding deferred to the
next turn.
* fix(ci): attempt 1 for PR #14286
Auto-generated by pr-dispatcher (task: 01KQBY8GAC1MNQCJ6T6X5DEP2F, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
* 🐛 fix(agent-marketplace): wire picker submit + fix marketplace-already-opened detection
The marketplace picker confirm flow was sending the user's selection back as a
synthetic user message, and the action hint kept telling the model to open the
marketplace again — leading to a death loop where the agent re-opened the
picker instead of summarizing + persisting + finishing onboarding.
Two issues:
1. Pick confirm forwarded the selection as a user message instead of forking
the agents and resuming from the tool result. Wire `prepareCustomInteractionSubmit`
into the intervention's submit branch so it runs `installMarketplaceAgents`
client-side and returns a descriptive `toolResultContent`. Plumb a
`createUserMessage: false` + `toolResultContent` option through
`submitToolInteraction` (slice + chat store): when set, skip the synthetic
user message, override the tool message content, and resume runtime from the
tool message (`parentMessageType: 'tool'`) so the LLM sees the install
result and continues from there.
2. `OnboardingActionHintInjector.marketplaceAlreadyOpened` read `msg.tool_calls`,
but this provider runs in pipeline phase 4.5 (virtual tail guidance) BEFORE
`ToolCallProcessor` (phase 5) converts DB-shape `tools` → OpenAI-shape
`tool_calls`. Detection always returned false → the hint kept saying
"call showAgentMarketplace" → death loop. Fix: match on `tools[].apiName`
(with `tool_calls` kept as a fallback). Also rewrote the Summary-phase hints
to reflect the new flow (picker resolves directly via tool result, no
synthetic user reply needed).
Includes intervention bar portal-target plumbing for approval actions.
* ✨ feat(onboarding): wire marketplace picker analytics on agent onboarding page
Mount AnalyticsBridge under AgentOnboardingPage to inject useAnalytics() into
setOnboardingAnalyticsClient, so onboarding_marketplace_shown/picked events
emit through PostHog instead of being silently dropped. Adds spm fields to
align with onboardingFeedback's telemetry shape.
* ♻️ refactor: move DEFAULT_ONBOARDING_MODEL to business-const
Made-with: Cursor
* ✨ test(customInteractionHandlers): add tests for persisting marketplace picks and resolutions
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(onboarding): enhance agent marketplace integration with metadata persistence
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(agent): add web onboarding agent selectors and integrate into Actions and Usage components
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
KnowledgeRepo queries use COALESCE(d.id, f.id) as id, which returns the
document's `docs_xxx` ID when a document exists for the file. Using this
as the proxy URL path (`/f/docs_xxx`) fails because the file proxy route
looks up the `files` table by `file_xxx` ID.
Fix: use `item.fileId` (always the actual file ID) for proxy URLs in
`getKnowledgeItems` and `recentFiles` handlers.
Closes#12196
* feat: support Line
* chore: update Line docs
* feat: support line platform
* chore: update markdown files
* fix: lint error
* fix: home padding block
Daily/weekly schedules dedup'd by calendar day, so a manual "run now"
earlier in the day would advance lastHeartbeatAt and make the dispatcher
skip today's scheduled tick. Dedup now compares against today's target
H:M instead — a 21:00 schedule still fires after a 18:00 manual run,
while post-target runs and same-tick re-dispatch are still skipped.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Azure / OpenAI strict structured outputs require every key in `properties`
to appear in `required`; the schema only listed `title` and `summary`,
so every generateHandoff call returned 400 "Missing 'keyFindings'".
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
♻️ refactor(context-engine): drop ____builtin suffix from tool names
Builtin tools now generate two-segment names like documents____upsertDocumentByFilename instead of documents____upsertDocumentByFilename____builtin. The "default" plugin type was already suffix-less, and "default" is no longer in active use, so collapsing builtin into the same shape removes redundant LLM-facing tokens. resolve() falls back to type 'builtin' for two-segment names and still parses legacy three-segment ____builtin names from message history.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(lobehub-skill): add video/image model lookup guide to generate reference
* docs(lobehub-skill): add full model type list and default-type warning to model reference
* docs(lobehub-skill): fix incorrect tip about lh model list default behavior
* 🐛 fix(builtin-skills): close template literal in model reference
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: remove schedule config popup from task list item
The task list row should only display the schedule trigger tag, not act
as an entry point for editing the automation. Configuration stays
available on the task detail page via TaskProperties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: mute BriefIcon when task is resolved
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: flatten task markdown card, drop container background and padding
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: expose task topic operationId and add copy menu item
Surfaces the persisted `task_topics.operationId` through the task detail API
so the topic card menu can offer a "Copy operation ID" entry alongside
"Copy topic ID", aiding debugging of completed runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: skip empty text block when Claude Code prompt is image-only
Anthropic rejects `{ text: '', type: 'text' }` with "messages: text content blocks must be non-empty", so uploading an image with no text would 400.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add topic actions menu and share button to task topic drawer
- Add "..." dropdown next to title with Copy topic ID / Copy operation ID
- Add Share icon next to close button, reusing SharePopover and ShareModal
- Pass topicId through SharePopover so it works outside the chat store scope
- Use getContainer={false} on Drawer to escape App's isolation stacking context, letting popups render above the drawer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: refactor home
* feat: add home agent id switch
* fix: useSend ensure agent map init
* feat: add custom image/video generation menu item
* chore: remove agent list ,group list and modetag
* fix: default home agent fallback
* fix: built in agent builder creation
* feat: add deepseek pro v4 hot picks
* chore: support agent select scrolling
* feat: add bot integration banner
* fix: lint error
* chore: update home page styles
* chore: adjust padding
* test: add image item to sidebar items test fixtures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: remove obsolete home starter e2e tests
The mode-tag buttons (Create Agent / Create Group / Write) no longer
exist after the Home refactor, so these scenarios cannot run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(home): collapse empty suggest questions wrapper on default home
Why: when enableAgentTask is on, SuggestQuestions and CommunityRecommend both render null on the default home view, but the AnimatePresence wrapper still mounted with marginTop:24 and produced a large empty gap between StarterList and DailyBrief.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): add cron-based task schedule dispatcher
Wires up backend execution for task-level cron schedules. Adds two
QStash workflows-hono routes:
- POST /api/workflows/task/schedule-dispatch — central sweep, point a
QStash Schedule (e.g. */30 * * * *) here. Loads all schedule-mode
tasks, filters by cron pattern + timezone + lastHeartbeatAt dedup,
and fans out per-task messages.
- POST /api/workflows/task/schedule-execute — internal per-task handler
that re-validates DB state and runs the task via TaskRunnerService.
Reuses existing schedulePattern / scheduleTimezone columns and
lastHeartbeatAt for dedup — no migration needed. Failure paths fall
through to the existing onTopicComplete error handling (urgent brief
+ paused).
* 💄 style(task): collapse resolved brief card on detail by default
Why: resolved briefs on the detail page rarely need re-reading; matching
home's collapse-when-resolved behavior keeps the activity feed compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(agent-profile): make popup header navigate to agent profile
Click on the avatar/title in AgentProfilePopup now closes the popup and routes to /agent/:id/profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): render task XML as a card in topic chat drawer
Why: the topic drawer's first user message is the task run prompt — a `<task>...</task>` XML blob (identifier, status, instruction, agent, …). Rendering it as raw XML buries the structure the user actually cares about.
- Add a `Task` markdown plugin (scope: user) that parses the `<task>` payload and renders an Artifacts-style card.
- Use a custom remark plugin so the block survives mdast splitting it across html + paragraph nodes.
- Gate the card UI behind a `TaskCardScope` React Context so it only activates inside `TopicChatDrawer`; everywhere else falls back to a plain `<pre>`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(claude-code): reuse result renders during streaming via wrapRender
Why: while a CC tool is still executing, the detail view fell back to a generic argument table for everything except `Agent`. Read/Write/Edit/Glob/Grep/Skill/Bash/TodoWrite already gracefully degrade their result Render when `content`/`pluginState` are absent, so the same component works for the live phase too.
- Add `wrapRender` helper that adapts a `BuiltinRender` into a `BuiltinStreaming` by passing `content: null`.
- Register Bash/Edit/Glob/Grep/Read/Skill/TodoWrite/Write streaming entries through `wrapRender`. `Agent` keeps its bespoke streaming view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task-subtasks): drop legacy blockedBy flattening branch
Why: subtasks now always arrive as a real tree from the upstream service, so the fallback that re-built the tree from a flat list via `blockedBy` is dead code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(view-switcher): hide chat/task switcher for heterogeneous agents
Why: the chat/task view switcher in the agent header doesn't apply when the agent is heterogeneous (Claude Code / Codex / etc.) — those agents don't share the task topic flow, so showing the switch surfaces a non-functional control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-topic): show elapsed duration on completed topic runs
Mirror task_topics terminal transitions (completed / failed / canceled / timeout)
onto topics.completedAt so the activity feed can render elapsed time for
finished runs, not just for the live one. Thread completedAt through
findWithHandoff and the TaskDetailActivity payload, then extend TopicCard
to render formatDuration(completedAt - createdAt) for non-running statuses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task-trigger-tag): respect automationMode when rendering schedule label
Heartbeat tasks were displaying cron schedule text when the DB still carried
a schedulePattern from a previous mode. Switch to automationMode as the
source of truth in TaskTriggerTag and pass it from all three call sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): stop topic scroll restore from corrupting itself
The restore path called scrollTo(snapshot.offset) one rAF after a fresh
VList mount, when only viewport-visible items had laid out. virtua
clamped the target against the still-incomplete scrollSize and landed
at offset 0, then the resulting onScroll fed back into recordScroll and
overwrote the snapshot to offset 0 — locking the user at the top on
every revisit.
Two fixes:
- Add a restoringRef guard that suppresses recordScroll while a
programmatic restore is in flight, released after two rAFs.
- Poll virtua's scrollSize for up to 30 frames until it can accommodate
the target offset before issuing scrollTo, with a safety bail-out so
unreachable offsets still resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): converge scroll snapshot to clamped offset on cap-out
When the saved offset is unreachable (e.g. messages were trimmed since
the snapshot was written), the polling loop hits its 30-frame cap and
falls through to scrollTo(targetOffset). Without this fix, the snapshot
keeps the stale unreachable offset, so every future revisit pays the
full polling delay before clamping again.
After the cap-out scrollTo lands, read the actual scrollOffset and
persist it (with a recomputed atBottom). Reachable-target restores still
leave the snapshot untouched so we don't churn writes for no reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: auto-dismiss upload dock after completion
UploadDock now auto-removes all completed files and hides itself 3 seconds
after all uploads finish (or error). If new uploads start during the timer,
the timer is cancelled and the dock stays visible.
Closes#9605
* fix(ci): 将 `useRef<ReturnType<typeof setTimeout>>()` 改为 `useRef<ReturnType<typeof setTimeout> | null>(null)`。
Auto-generated by pr-dispatcher (task: 01KQ9ZB50GQXWTYADHAWEGTNQR, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): Guarded `clearTimeout(autoDismissTimerRef.current)` calls with `if (autoDismissTimerRef.current)` checks in the UploadDock auto-dismiss effect.
Auto-generated by pr-dispatcher (task: 01KQA0NZB57SFPHP45227ENZAT, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 💄 style(task): replace page drawer with modal and rebuild artifact card
- Migrate page preview from a right-side drawer to a centered modal
(`PageModal`) with allow-fullscreen support; rename store state
`activePageDrawerPageId` → `activePageModalId` and the corresponding
`openPageDrawer` / `closePageDrawer` actions / selectors.
- Refresh artifact cards: collapse to a single-line layout (smaller
file icon, inline size + identifier tag) and add a remove action
that calls `unpinDocument` against the artifact's `sourceTaskId`
fallback chain (so artifacts pinned from another task unpin from
the right task, not just the active one).
- Surface `sourceTaskId` on `TaskDetailWorkspaceNode` /
`WorkspaceDocNode` and through the task service so the renderer
can resolve the owning task for the unpin call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): add delete action for brief cards
- `briefService.delete` calls `brief.delete` mutation; `deleteBrief`
store action removes the brief from the in-memory list after the
server roundtrip.
- `TaskBriefCard` exposes a `MoreHorizontal` dropdown with a danger
delete item gated by an `App.confirm` modal; `TaskActivities`
passes `onAfterDelete=refreshActiveTask` so the activity list
re-fetches once the brief is gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): use local timezone over DB-default UTC on first schedule enable
The `tasks` table seeds `schedule_timezone` to `'UTC'` on row creation, so
even a task that has never been scheduled surfaces `timezone='UTC'`. The
previous "if timezone is missing, use local" check therefore never fired,
and first-time schedule enable always defaulted to UTC.
Treat a missing `pattern` as the reliable signal that the user has never
opened the schedule form, and override the DB-default UTC with the user's
local IANA zone in that case. A user-chosen timezone (with a real
pattern) is still preserved on subsequent re-entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/scheduler): replace TimePicker with half-hour Select
- Cron storage rounds minutes to 0/30 (see `buildCronPattern`), so the
picker only ever needs 48 half-hour slots — flatten antd's
hour×minute grid into a single-column `Select`.
- Anchor every dropdown (`getPopupContainer`) inside the parent Base UI
Popover so option clicks aren't treated as outside-clicks (which
dismissed the popover before the selection committed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task/subtasks): wire context menu via Tree.onRightClick
`ContextMenuTrigger` was attached to each subtask title's inner
`Flexbox`, but antd `Tree`'s row-level `.ant-tree-node-content-wrapper`
only `preventDefault`s the contextmenu event when an `onRightClick`
handler is provided. Right-clicks landing in the row gap (anywhere
outside the title element) fell through to the browser's native menu.
- Refactor `useTaskItemContextMenu` into a shared
`useTaskContextMenuActions` factory exposing stable
`buildItems(task)` / `installKeyboardHandlers(task)`. Existing
`useTaskItemContextMenu(task)` API is preserved as a thin wrapper.
- `TaskSubtasks` now calls `Tree.onRightClick`, looks up the subtask
by `node.key` from a recursively-built map (subtasks are returned
as a nested tree, not flat), and calls `showContextMenu` plus the
keyboard-handler installer imperatively.
- The flat-map walk is recursive so right-click works on nested
children, not just top-level subtasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/topic): wrap dropdown to swallow card click + relabel topic ID
- Wrap the topic card's `MoreHorizontal` dropdown in a `Flexbox`
with `onClick={stopPropagation}` so menu interactions don't
bubble through to the card-level click handler.
- Fix the menu label fallback: `Copy run ID` → `Copy topic ID` to
match what the action actually copies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task/artifacts): also refresh active task SWR after unpin
`unpinDocument` is called with `node.sourceTaskId` (the task that
owns the pin row, often a descendant DB id), but the open detail
page's SWR cache is keyed by `activeTaskId` (typically the parent
identifier from `/task/{identifier}`). Refreshing only the source
key left the parent's workspace stale until reload.
After the unpin succeeds, also revalidate the active key when it
differs from the source. The server call still uses the source id
because `model.unpinDocument` deletes by exact `(taskId, documentId)`
match — passing the parent identifier would no-op for docs pinned
by a subtask.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(panel): give page and task right panels independent visibility
Page editor and Task layout now read/write `showPageAgentPanel` /
`showTaskAgentPanel` (with matching `togglePageAgentPanel` /
`toggleTaskAgentPanel` actions) instead of sharing the global
`showRightPanel`, so toggling one no longer flips the other. Task panel
defaults to collapsed; page panel stays open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/detail): tighten artifact size label and align activity card padding
- artifact size shows raw count with "字" instead of "1.4k 字符"
- swap artifact file icon to FileTextIcon (lucide), 18px
- BriefCard padding 12 → paddingInline 8 to align with CommentInput; BriefIcon 20 → 24
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task/page-modal): give modal its own header via PageEditor slot
PageEditor now accepts an optional `header` slot (undefined keeps the
built-in Header, null hides it). PageModal stops relying on antd's title
chrome and supplies its own header — title + autosave on the left, panel
toggle and close on the right — so the modal no longer stacks two
headers and owns its own composition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(page): mirror document into pageStore on standalone fetch
Document fetch now upserts the loaded `page`-source document into
pageStore via a new `upsertDocument` action. PageExplorer reads title
and emoji from pageStore selectors, so opening a page from a context
that never hit the page list (e.g. the task workspace modal) used to
show empty title/emoji until the list was visited.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): polish schedule popover
Refresh the schedule popover after design review:
- Header: avatar with ⚡ icon + summary (e.g. "Runs every 10 min" / "Daily
at 09:00 · China Standard Time"); next-run preview block under the title.
- Segmented tabs gain Calendar / Refresh icons; Recurring tab drops the
Clear button + advanced section (only Schedule mode keeps advanced).
- Advanced settings is now an Accordion (matches lobehub patterns) and
hosts timezone + max executions.
- All inputs switch to variant="filled"; weekday picker uses
colorPrimaryBg + colorPrimary instead of solid primary to fix the
white-on-white "burned" active state.
- Popover surface uses colorBgContainer + colorBorderSecondary border +
12px radius for clearer elevation.
New `scheduler/helpers.ts` formats the cron summary, resolves IANA
timezone display names via Intl, and computes the next firing time for
both heartbeat and cron schedules (uses dayjs/plugin/timezone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): hide standalone "Brief" fallback in task list
When a brief activity has no title/summary AND no briefType, the latest
activity line on the task list rendered just "Brief" / "简要" — useless
text with no actual content. Return undefined in that case so the line
is omitted entirely.
Drops the now-unused `taskDetail.latestActivity.briefOnly` key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): navigate to /page/:id when clicking artifact tree
Drop `selectable={false}` on the workspace tree and wire `onSelect` to
push `/page/<documentId>`, so artifacts are openable from the task
detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): enforce 10-minute minimum on recurring interval
Drop the Seconds unit from the Recurring tab so users can't schedule
sub-minute intervals (which the runner can't keep up with anyway), and
clamp existing values that are smaller than 10 minutes to 10 minutes
when the popover opens.
Drops the now-unused `taskSchedule.seconds` key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): surface needs-review group above backlog in task list
Reorder the default kanban/list groups so `needsInput` (paused + failed)
sits at the top — the list view stacks groups vertically, and putting
actionable items first means users see what needs attention before
scrolling past long backlogs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): catch up next heartbeat firing past stale lastAt
When `lastAt + interval` already lies in the past (e.g. task was paused
for hours), step forward by whole intervals so the returned time is
strictly after now. Otherwise the popover would show a stale
"next run" timestamp until the next tick lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): open artifact pages in right-side drawer
Replace the `/page/:id` navigation from the artifact tree (a4af053338)
with a right-side drawer that shows the page in-place — the same UX
pattern as the chat document portal, so users keep the task context
while previewing artifacts.
- New `PageDrawer` mirrors `TopicChatDrawer` styling (right-anchored
floating drawer with rounded edges + shadow). Renders `PageExplorer`
inside.
- Task store gains `activePageDrawerPageId` state with
`openPageDrawer` / `closePageDrawer` actions; opening a page also
closes the topic drawer so the two don't stack on the same edge.
- `TaskArtifacts.onSelect` now calls `openPageDrawer(documentId)`
instead of pushing a new route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): seed defaults when entering an automation mode
Switching to a mode without persisting its core fields left the task in
a "mode enabled but unconfigured" state — the popover showed
"自动化未启用" / "Automation is off" because schedulePattern was still
null even though the Schedule tab was active, and the cron runtime had
nothing to fire.
`setAutomationMode` now seeds:
- `heartbeatInterval = 600` (10 min) when entering heartbeat without one
- `schedulePattern = '0 9 * * *'` + `scheduleTimezone = 'UTC'` when
entering schedule mode without them
Existing values are preserved on subsequent mode toggles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): default scheduleTimezone to user's local IANA zone
Hardcoding `UTC` meant a user in Shanghai who picked "Daily 09:00" on a
fresh task would actually fire at 17:00 local. Resolve the user's local
zone via `Intl.DateTimeFormat().resolvedOptions().timeZone` (with a UTC
fallback for environments where Intl is unavailable) so the seeded
default matches what the user expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): polish list, detail, and schedule UI
- Always show top-right + button in kanban view (no inline create input there)
- Unify subtasks/artifacts/activities section indicator on the Accordion arrow
- Refresh schedule popover nextRun every minute and move styling to staticStyles
- Move paused/failed groups ahead of running/backlog in task list ordering
- Color the scheduled status icon with colorWarning to match other active states
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): gate reconnect on server URL, not user toggle
Resuming a Gateway-running operation should depend on whether the server has
a Gateway URL configured — the user's lab toggle controls *new* requests, not
reattaching to an op that's already running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): surface scheduled state with cancel action and countdown
- Reorder list view group ranks so paused/failed (待审阅) sit above
running and backlog, matching the kanban needsInput-first layout.
- Map `scheduled` task status to the running group so cron/heartbeat
tasks waiting between ticks no longer fall through to backlog.
- Render a muted "Scheduled" pill on task list rows so users can tell
scheduled (waiting) apart from running (executing now) at a glance.
- Add a "Cancel schedule" action and live countdown to the task detail
page when status=scheduled; cancel disables automation AND moves the
task back to backlog so the status badge updates immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): redesign artifact list as flat cards with file icons
Replace the antd Tree-based artifact view with a flat list of clickable
outlined cards. Each card uses FileIcon (resolves a real file glyph from
the title's extension) and shows the artifact title, size, and source
task tag inline. Removes the unused folder/tree visualization since
workspace nodes today are effectively flat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): use warning color for scheduled status icon
Promote the scheduled status icon from `colorTextDescription` to
`colorWarning` so it visually groups with `running` (also warning) — both
states represent "automation in progress" and now share a consistent
warm color, matching how kanban groups them in the same column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(topic): use shared MAIN_SIDEBAR_EXCLUDE_TRIGGERS constant
Replace the local EXCLUDE_TRIGGERS array with the canonical
MAIN_SIDEBAR_EXCLUDE_TRIGGERS exported from `@/const/topic` so the chat
sidebar and any other consumers stay aligned on which trigger types are
hidden from the main topic list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): rename artifact label from 作品 to 产物 in zh-CN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): align artifact cards with activities content width
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): collapse resolved brief cards by default
Resolved brief cards now show only the header row with a "marked as resolved" badge and an expand chevron; clicking the chevron reveals the summary and actions. Also tightens the collapsed summary max-height from 240 to 180.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): show human-readable schedule on trigger tag
The list/properties trigger tag rendered the raw cron pattern
("0 9 * * * (Asia/Shanghai)") which is unreadable for non-engineers.
Reuse the popover's `formatScheduleDescription` + `formatTimezoneName`
helpers so the tag now reads as e.g. "每天 09:00 执行 · 中国标准时间".
The raw cron + IANA id moves into the tooltip for users who need it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): split timezone onto a smaller secondary line
The schedule summary used to read "每天 09:00 执行 · 中国标准时间" on a
single line, which crowded the popover header and the inline trigger tag
in TaskProperties. Move the timezone onto its own line below the
description with a smaller font and `colorTextDescription`, so the
primary information (when it fires) reads cleanly first.
For the compact pill (`mode='tag'`) used in the task list, drop the
visible timezone entirely — it stays accessible via the tooltip
alongside the raw cron pattern.
Drops the now-unused `taskSchedule.summary.schedule` interpolation key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): default to schedule mode + reword automation copy
- Toggle "自动化" on now lands in the Schedule tab (cron) instead of the
Heartbeat tab. A scheduled run is the more common, predictable choice
— users who want fixed intervals can switch tabs from there.
- Rename the heartbeat tab from "循环任务"/"Recurring" to "心跳模式"/
"Heartbeat" so the term matches the underlying mechanism (and the
existing `taskSchedule.tag.heartbeat` copy).
- Replace 执行 with 运行 across the schedule UI strings (持续执行 → 持
续运行, 执行频率 → 运行频率, 下次执行 → 下次运行, etc.) for a more
natural "run" framing.
- Drop dead keys `taskSchedule.interval` and `taskSchedule.schedulerNotReady`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): resolve brief and re-run task on free-form feedback
The SquarePen feedback editor only called addComment, leaving the
urgent brief unresolved — so the heartbeat re-arm gate kept skipping
the task with reason=human-waiting and the card never moved. Switch
the path to submitFeedback (resolveBrief + task.run) so the agent
picks up resolvedComment on the next turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): make trigger tag hover human-readable too
The pill already shows "每天 09:00 运行", but the tooltip still leaked
the raw cron + IANA id ("0 9 * * * (Asia/Shanghai)") on hover. Replace
it with a single readable line using "·" as separator, e.g.
"每天 09:00 运行 · 中国标准时间".
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: add userId and serverId tooltip guide
* feat: update built in message tool
* ✨ feat(cli): add bot dm-policy / allowlist subcommands (LOBE-8254)
Extend `lh bot update` with --dm-policy / --group-policy / --user-id /
--server-id, and add new `lh bot allowlist` and `lh bot group-allowlist`
subcommand groups (list/add/remove/clear). All write paths read existing
settings first and merge so unrelated keys aren't wiped by the partial
update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(channel): warn when a saved bot is missing the operator userId
Surface an inline alert and auto-expand the Advanced Settings group when an
existing bot has no settings.userId — without it AI tools can't push
notifications back to the operator and pairing approvals fail silently.
Skip on first-time configs and on platforms that don't expose userId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: optimize userId alert
* fix: test case
* fix: footer effective userId
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
description: Guide for adding new AI provider documentation. Use when adding documentation for a new AI provider (like OpenAI, Anthropic, etc.), including usage docs, environment variables, Docker config, and image resources. Triggers on provider documentation tasks.
description: Guide for adding environment variables to configure user settings. Use when implementing server-side environment variables that control default values for user settings. Triggers on env var configuration or setting default value tasks.
description: Build or extend LobeHub Agent Signal pipelines for background or quiet agent work driven by event sources, semantic signals, and action handlers. Use when adding a new Agent Signal source, signal or action type, policy, middleware handler, workflow handoff, dedupe or scope behavior, or observability around `src/server/services/agentSignal/**`, `packages/agent-signal`, or `packages/observability-otel/src/modules/agent-signal`.
---
# Agent Signal
Use this skill to implement event-driven background work for agents without coupling the work to the foreground chat request.
1. Read `references/architecture.md` to map the package boundary, runtime queue, scope model, and async workflow handoff.
2. Read `references/handlers.md` before writing any new policy, source handler, signal handler, or action handler.
3. Read `references/observability.md` when you need tracing, metrics, debugging, or workflow snapshot visibility.
## Use The Right Entry Point
- Use `emitAgentSignalSourceEvent(...)` when a server-owned producer should execute the pipeline immediately.
- Use `executeAgentSignalSourceEvent(...)` when a worker or controlled backend path already owns execution timing and may inject a runtime guard backend.
- Use `enqueueAgentSignalSourceEvent(...)` when the caller should return quickly and let Upstash Workflow process the event out-of-band.
- Use `emitAgentSignalSourceEventWithStore(...)` for isolated tests or evals that should avoid ambient Redis state.
Read:
-`src/server/services/agentSignal/index.ts`
-`src/server/workflows/agentSignal/index.ts`
-`src/server/workflows/agentSignal/run.ts`
## Core Model
-`source`: A normalized fact that happened. Sources come from producers such as runtime lifecycle events, user messages, or bot ingress.
-`signal`: A semantic interpretation derived from one source or from another signal. Signals express meaning, routing, or policy state.
-`action`: A concrete side effect planned from one signal. Actions do the work.
-`policy`: An installable middleware bundle that registers source, signal, and action handlers.
-`procedure`: Not a distinct runtime node. Treat "procedure" as the end-to-end flow for one use case: ingress source, matching handlers, planned actions, execution result, and observability.
Keep the boundaries strict:
- Add a new `source` when the outside world produced a new event.
- Add a new `signal` when the system needs a reusable semantic interpretation.
- Add a new `action` when the runtime needs a concrete side effect.
- Add or update a `policy` when you are wiring those pieces together.
## Implementation Workflow
1. Decide whether the use case is synchronous or quiet background work.
2. Define or reuse a source type in `src/server/services/agentSignal/sourceTypes.ts`.
3. Define or reuse signal and action types in `src/server/services/agentSignal/policies/types.ts`.
4. Implement handlers with `defineSourceHandler`, `defineSignalHandler`, or `defineActionHandler`.
5. Bundle handlers with `defineAgentSignalHandlers(...)`.
6. Register the policy in `src/server/services/agentSignal/policies/index.ts` and pass it into the runtime factory if needed.
7. Add or update ingress code that emits or enqueues the source event.
8. Add observability and tests before considering the flow complete.
- Reuse existing source, signal, and action types before adding new ones.
- Keep source handlers focused on interpretation and fan-out, not heavy side effects.
- Keep action handlers responsible for side effects, idempotency, and executor-style result reporting.
- Use stable ids and idempotency keys when the same source can arrive more than once.
- Preserve scope discipline. The runtime uses `scopeKey` to serialize related background work.
- Prefer the dedicated shared package types and builders from `@lobechat/agent-signal` for normalized nodes and result contracts.
- Add focused tests near the touched runtime, policy, or store module. Existing tests under `src/server/services/agentSignal/**/__tests__` are the reference pattern.
## References
- Architecture and boundaries: `references/architecture.md`
- Writing handlers and policies: `references/handlers.md`
- Observability, metrics, and debugging: `references/observability.md`
- a trace envelope with source, signals, actions, results, edges, and handler runs
- a compact telemetry record with dominant path, status breakdown, and chain metadata
This projection is built from:
- source node
- emitted signals
- planned actions
- executor results
## How To Inspect A Chain
Use this order:
1. Inspect the source type and payload.
2. Inspect emitted signals.
3. Inspect planned actions.
4. Inspect executor results.
5. Inspect projected edges and dominant path.
The helper `toAgentSignalTraceEvents(...)` flattens a chain into compact event records suitable for tracing snapshots.
## Workflow Snapshot Bridge
Workflow-triggered runs do not naturally pass through the normal foreground runtime snapshot path, so `runAgentSignalWorkflow` adds a development-only bridge into `.agent-tracing/`.
Read:
-`src/server/workflows/agentSignal/run.ts`
Use that path when:
- the source was enqueued with `enqueueAgentSignalSourceEvent(...)`
- you need local trace visibility for quiet background work
## Common Debug Questions
### The source emits but nothing happens
Check:
- feature gate enabled for the user
- source type matches a registered source handler
- dedupe or scope lock did not short-circuit generation
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`).
`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.
→ 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:
- **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**.
- 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`:
`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`):
`ClientFactory.validateCredentials` is called from the TRPC `testConnection` mutation — implement it to hit the platform API and return useful per-field errors.
- 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.
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.
-`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.
description: Build a new builtin tool package under `packages/builtin-tool-<name>/`. Use when adding a new agent-callable toolset, designing its API surface (manifest / ApiName / Params / State), implementing the Executor + ExecutionRuntime, building the Inspector / Render / Placeholder / Streaming / Intervention / Portal UI, or wiring a tool into the central registries (`packages/builtin-tools/src/{index,identifiers,inspectors,renders,placeholders,streamings,interventions,portals}.ts` and `src/store/tool/slices/builtin/executors/index.ts`). Triggers on "new builtin tool", "add a tool", "tool inspector", "tool render", "tool placeholder", "tool streaming", "tool intervention", "BuiltinToolManifest", "BaseExecutor", "ExecutionRuntime".
---
# Builtin Tool Authoring Guide
A builtin tool is a package the agent runtime can call. It ships **five faces**:
| Where do files live? What does each face do? Wiring? | [architecture.md](references/architecture.md) |
| How do I name the tool, design APIs, write the manifest, executor, ExecutionRuntime? | [tool-design.md](references/tool-design.md) |
| How do I build Inspector / Render / Placeholder / Streaming / Intervention / Portal? | [ui.md](references/ui.md) |
---
## When to Use This Skill
- Creating a new `packages/builtin-tool-<name>/` package
- Adding a new API method to an existing builtin tool
- Building or restyling any of the 6 client surfaces for a tool
- Wiring a tool into the central registries
- Debugging "tool not found / API not found / render not showing / placeholder stuck" errors
---
## Top-Level Design Principles
1.**`lobe-<domain>` identifier is permanent.** It's stored in message history. Renames need `@deprecated` aliases (see `packages/builtin-tools/src/inspectors.ts:88-89`). Get it right the first time.
2.**ApiName is an `as const` object**, not a TS enum. It doubles as the runtime list `BaseExecutor` iterates over.
3.**Three result fields, three audiences:**
-`content: string` → the LLM reads it
-`state: Record<…>` → the UI's `pluginState`; **result-domain only**, never echo all params back
-`error: { type, message, body? }` → both LLM and UI; `type` is a stable code
4.**Split execution from frontend wiring.**
-`src/ExecutionRuntime/` — pure runtime, no React, no Zustand, accepts services via constructor. **The default place for new logic.**
-`src/client/executor/` — `BaseExecutor` subclass that calls `ExecutionRuntime` (or stores/services directly when frontend-only).
5.**UI defaults to "do nothing".** Inspector is required (the header strip). Render/Placeholder/Streaming/Intervention/Portal are added **only when there's something specific to show** — empty registries are fine.
6.**Style with `createStaticStyles + cssVar.*`** (zero-runtime). Fall back to `createStyles + token` only when you genuinely need runtime values. Use `@lobehub/ui` components, not raw antd.
7.**i18n keys live in `src/locales/default/plugin.ts`.** Inspector titles must come from `t('builtins.<identifier>.apiName.<api>')` so something renders while args stream.
└── components/ # shared subcomponents used by the surfaces above
```
**Older packages** (`builtin-tool-task`, `builtin-tool-calculator`, etc.) still have `src/executor/` as a sibling of `src/client/`. That's grandfathered; **don't relocate without a deliberate refactor**. New packages and new APIs added to existing packages should follow the layout above.
| Pure-compute, no UI state | `packages/builtin-tool-calculator/` — `ExecutionRuntime` reuses executor (mathjs/nerdamer work everywhere) |
| CRUD over a domain entity | `packages/builtin-tool-task/` — full Inspector + Render set, batch variants |
| Heavy UI (Inspector/Render/Placeholder/Portal) | `packages/builtin-tool-web-browsing/` — search-style result UI, Portal for detail view |
| Desktop / filesystem with all surfaces (incl. Streaming + Intervention) | `packages/builtin-tool-local-system/` — `ExecutionRuntime` injects an `ILocalSystemService`, executor calls it |
| Server-side pure (no client executor) | `packages/builtin-tool-web-browsing/` — only `ExecutionRuntime` is exported; the chat client doesn't run it |
| Needs human approval before running | `packages/builtin-tool-local-system/src/client/Intervention/` — per-API approval components |
- Server bundles import only `./` and `./executionRuntime` and never touch React.
- Frontend bundles import `./client` and never touch Node-only services.
- The runtime is testable without React or Electron present.
---
## Why ExecutionRuntime is the Default Home for Logic
**Old pattern (grandfathered):** business logic in `src/executor/` directly. Examples: `builtin-tool-task`, older tools. Works, but the executor mixes runtime logic with frontend service plumbing — hard to reuse on the server.
**New pattern (preferred):** business logic in `src/ExecutionRuntime/`, frontend wiring in `src/client/executor/`. Examples: `builtin-tool-local-system`, `builtin-tool-web-browsing`, `builtin-tool-calculator`.
```
ExecutionRuntime
├─ accepts services via constructor (or `static create(opts)`)
├─ returns BuiltinServerRuntimeOutput (content + state + success)
└─ no React, no Zustand, no `@/services/...` direct imports
client/executor
├─ extends BaseExecutor<typeof <Name>ApiName>
├─ holds a `runtime = new <Name>ExecutionRuntime(realService)` instance
├─ each ApiName method:
│ 1. resolve scope / pull defaults from BuiltinToolContext
│ 2. call runtime.<method>(args)
│ 3. funnel through toResult() → BuiltinToolResult
└─ exported singleton: export const <name>Executor = new <Name>Executor()
```
### Service injection
`ExecutionRuntime` should declare a TypeScript interface for the services it needs and accept the implementation via constructor. Server callers wire in real implementations; tests wire in mocks. Example from `local-system`:
### When ExecutionRuntime is the only thing you ship
Some tools are server-only — there's no frontend executor. `builtin-tool-web-browsing` is the canonical example: only `./` and `./executionRuntime` are exported, no `./executor`, and the runtime is constructed by the server-side `ToolExecutionService`. Skip `client/executor/` entirely for those.
### When the executor reuses the runtime as-is
Pure-compute tools (`builtin-tool-calculator`) often have an executor whose ApiName methods call `executor.calculate(args)` and an `ExecutionRuntime` whose methods call `calculatorExecutor.calculate(args)` — same logic, two thin wrappers. That's fine; the duplication buys you the bundle split.
content: string;// the LLM-facing text — never undefined; default to error message
state?: any;// result-domain object the UI reads as pluginState
success: boolean;// mandatory
error?: any;// raw error; the executor will repackage
}
```
### `BuiltinToolResult` (what the executor returns to the runtime)
```ts
{
success: boolean;
content?: string;
state?: any;
error?:{type:string;message: string;body?: any};
metadata?: Record<string,any>;// rare; e.g. { agentCouncil: true }
stop?: boolean;// rare; halt the orchestration step
}
```
### The `toResult` funnel (mandatory)
Every executor method returns through a single `toResult()` to enforce two invariants:
1.**`content` is never undefined.** A missing content collapses downstream into `''`, leaving the Debug pane blank while `pluginState` was already saved. See the `globLocalFiles` regression in `local-system/src/client/executor/index.ts:60-84`.
2.**`state` survives failures.** Renderers can keep showing partial output even when `success: false`.
return(thisasany)[apiName](params,ctx);// method name MUST equal apiName value
```
So:
- **Method names must equal `<Name>ApiName` values, exactly.** A typo silently routes to "ApiNotFound".
- **Methods must be class fields, not class methods**, because `this` is lost when registry calls `executor.invoke(apiName, params, ctx)`. Always declare as `methodName = async (…) => { … }`.
- **Always destructure `apiEnum` and `identifier` as `readonly` instance fields**, not getters — `BaseExecutor.hasApi/getApiNames` reads them synchronously.
---
## `BuiltinToolContext` — What the Executor Receives
The runtime hands every executor method an optional `BuiltinToolContext` as the second argument:
For dev preview, also seed `locales/zh-CN/plugin.json` and `locales/en-US/plugin.json`. Run `pnpm i18n` before opening a PR — it's slow, so do it once at the end. (See the **i18n** skill for the full workflow.)
---
## Registry Wiring
Five core files plus optional ones. Miss any and you'll see "tool not found", a missing chip, a blank result card, a stuck spinner, or an approval dialog that never appears.
| `packages/builtin-tools/src/index.ts` | Import `<Name>Manifest`; push entry to `builtinTools`. Set `hidden`/`discoverable` flags. |
| `packages/builtin-tools/src/identifiers.ts` | Add `<Name>Manifest.identifier` to `builtinToolIdentifiers`. |
| `packages/builtin-tools/src/inspectors.ts` | Import `<Name>Inspectors, <Name>Manifest`; add to `BuiltinToolInspectors`. |
| `src/store/tool/slices/builtin/executors/index.ts` | Import `<name>Executor`; add to `registerExecutors([…])`. |
| **Conditional — add only if the surface exists** | |
| `packages/builtin-tools/src/renders.ts` | Add to `BuiltinToolsRenders` if any API has a Render. |
| `packages/builtin-tools/src/placeholders.ts` | Add to `BuiltinToolPlaceholders` if any API has a Placeholder. |
| `packages/builtin-tools/src/streamings.ts` | Add to `BuiltinToolStreamings` if any API has a Streaming renderer. |
| `packages/builtin-tools/src/interventions.ts` | Add to `BuiltinToolInterventions` if any API has an Intervention component. |
| `packages/builtin-tools/src/portals.ts` | Add to `BuiltinToolsPortals` if the tool has a Portal. |
| `packages/builtin-tools/src/displayControls.ts` | Add if Render must show/hide based on result content (rare; see ClaudeCode/Codex). |
### Optional flags in `packages/builtin-tools/src/index.ts`
```ts
{
identifier: TaskManifest.identifier,
manifest: TaskManifest,
type:'builtin',
hidden: true,// hide from chat-input Tools popover
discoverable: false,// exclude from agent builder / skill discovery
}
```
Lists in the same file you may need to touch:
-`defaultToolIds` — added to the agent's tool list by default
-`alwaysOnToolIds` — forced on regardless of user selection (use sparingly)
-`runtimeManagedToolIds` — enable state controlled by runtime, not user UI; **must mirror the rules map** in `src/server/modules/Mecha/AgentToolsEngine/index.ts` and `src/helpers/toolEngineering/index.ts`
This doc covers everything that **isn't UI**: the tool's identifier, API surface, manifest, types, system prompt, ExecutionRuntime, and the executor that wires it into the frontend.
For UI surfaces (Inspector / Render / Placeholder / Streaming / Intervention / Portal), see [ui.md](ui.md).
For where files live and how registries work, see [architecture.md](architecture.md).
- **`lobe-` prefix is mandatory** — many switches in the codebase key off it.
- Pick a **domain noun**, not a verb (`lobe-task`, not `lobe-task-manager`).
- The identifier is **persisted in message history** — renaming after release means the `@deprecated` alias trick (register the legacy identifier as a second key in `inspectors.ts` / `renders.ts` pointing at the new module). Get it right the first time.
- **Plural variant for batch** (`createTasks`, `runTasks`) — describe in the manifest description that it's preferred over multiple single calls. The system prompt should also push the batch form.
- Reserve **clear separation between mutating verbs** (`updateTaskStatus`, `editTask`) and **execution verbs** (`runTask`). The system prompt must warn the model when these are confusable — see `task` for the canonical "do NOT use updateTaskStatus(running) to start a task" warning.
Define `<Name>ApiName` as `as const` so it doubles as a runtime enum (used by `BaseExecutor`) and a literal type. Then declare `Params` and `State` per API.
```ts
exportconstTaskIdentifier='lobe-task';
exportconstTaskApiName={
createTask:'createTask',
createTasks:'createTasks',
listTasks:'listTasks',
/* …one entry per API, group logically (CRUD then run-style) */
**The result-domain rule for `State`** (memory: "pluginState is result-domain, not call-domain"):
- Include only fields the UI **renders after the call returns** — ids the LLM didn't have when calling, counts, summary numbers, server-assigned status.
- **Don't echo all params.** The Inspector/Render gets `args` for free.
- Keep batch results as `{ succeeded, failed, results }` so the Render can show a one-line summary plus a detail list.
description:'Detailed instruction for what the task should accomplish.',
},
parentIdentifier:{
type:'string',
description:
'Identifier of the parent task (e.g. "TASK-1"). If provided, the new task becomes a subtask.',
},
priority:{
type:'number',
description:'Priority level: 0=none, 1=urgent, 2=high, 3=normal, 4=low. Default is 0.',
},
},
},
},
/* …one entry per ApiName */
],
};
```
### Manifest writing checklist
- **Every API in `<Name>ApiName` has exactly one entry in `api[]`.** Easy to drift after a refactor.
- **`description` on each API is the model's only docs.** Make it long enough for the LLM to pick the right tool. Mention edge cases ("If you provide any filter, omitted filters are not applied implicitly"), defaults, and the relationship to sibling APIs ("To START a task, use runTask — updateTaskStatus only flips a flag").
- **`parameters` is JSON Schema** (`LobeChatPluginApi`). Use `enum`, `required`, `items`, `oneOf`, `additionalProperties: false` etc. — these survive into the LLM's tool spec.
- **Use `additionalProperties: false`** on parameter objects so the model can't sneak unknown fields past validation.
- **Number parameters with semantic values** (`priority: 0=none, 1=urgent, …`) should describe the mapping in the description. Don't rely on `enum` alone for numbers — the model often fills the wrong one.
- **`enum` arrays for known string sets** (statuses, categories, engines). Spread from a constants module (`enum: [...TASK_STATUSES]`) so the manifest stays in sync.
### Optional manifest fields
```ts
{
/* Where this tool can run.
'client' → Agent Gateway dispatches to the desktop client (filesystem, Electron only)
'server' → ToolExecutionService runs it on the server
omitted → server only */
executors:['client','server'],
/* Default human intervention policy for all APIs that don't specify one.
Pair with an Intervention component (see ui.md). */
Per-API `humanIntervention` and `renderDisplayControl` go inside each `api[]` entry.
---
## 4. `systemRole.ts` — Operator Instructions for the Model
This is appended to the agent system prompt whenever the tool is enabled. Treat it as a **how-to-use guide for the LLM**, not marketing copy.
```ts
exportconstsystemPrompt=`You have access to Task management tools. Use them to:
- **createTask**: Create a new task. Use parentIdentifier to make it a subtask.
- **createTasks**: Prefer this over multiple createTask calls when planning a batch
(e.g. all subtasks under one parent, or all chapters of an outline).
- **runTask**: Actually START a task — kicks off the agent in a new (or continued)
topic. Do NOT use updateTaskStatus(running) to start a task; that only flips a
flag without executing. The task must have an assigneeAgentId.
- **updateTaskStatus**: Change a task's status (completed/cancelled/paused/failed).
If you mark a task as failed, include an error message explaining why.
- ...
When planning work:
1. Create tasks for each major piece (use parentIdentifier to organize as subtasks).
2. Use editTask with addDependencies to control execution order.
3. Use updateTaskStatus to mark the current task completed when done.`;
```
### Patterns that work well
- **Bulleted list, bold the API name, one line per API.** The model picks tools by skimming.
- **Disambiguate confusable APIs explicitly** (`runTask` vs `updateTaskStatus`).
- **Push toward batched APIs** ("Prefer this when…").
- **End with a numbered workflow** if the tool has a typical sequence.
- **For tools with multiple environments** (e.g. desktop vs cloud), keep variants in `systemRole.ts` and `systemRole.desktop.ts` and pick at the manifest level. See `builtin-tool-local-system`.
### Dynamic system prompts
If the prompt depends on runtime state (current date, available models), export a function and call it in the manifest:
```ts
// systemRole.ts
exportconstsystemPrompt=(today: string)=>`Today is ${today}. You have web search tools…`;
Use when the same logic runs in browser and Node (e.g. mathjs, nerdamer). The runtime is a thin wrapper that imports the executor and re-types the state per API. See `builtin-tool-calculator/src/ExecutionRuntime/index.ts` for the canonical example.
### Pattern C: Extend a shared base
When you're implementing a domain that already has a base runtime (file ops via `ComputerRuntime`), extend and only override `callService` + result normalization. See `builtin-tool-local-system/src/ExecutionRuntime/index.ts`.
### Runtime contract
Every method returns:
```ts
{
content: string;// LLM-facing — never undefined; default to error message
state?: any;// result-domain — what the UI's pluginState becomes
success: boolean;// mandatory
error?: any;// raw error object; the executor will repackage
}
```
Use `@lobechat/prompts` formatters (`searchResultsPrompt`, `crawlResultsPrompt`, `formatTaskCreated`, etc.) to produce structured `content`. They emit XML/markdown that's already tuned for token efficiency.
The executor's job is to **resolve frontend defaults** (current agent, current task, scope) and **call the runtime**. It then funnels through `toResult()` into the `BuiltinToolResult` shape.
1.**Methods are class fields** (`name = async (…) => {…}`), not class methods. The registry calls `(executor as any)[apiName](params, ctx)`; arrow-function fields keep `this` bound.
2.**`identifier` and `apiEnum` are `readonly` instance fields**, not getters — `BaseExecutor.hasApi/getApiNames` reads them synchronously at registration time.
3.**Default missing params from `ctx`**, but never silently override explicit values. Use `params.foo ?? ctx?.foo`, not `ctx?.foo ?? params.foo`.
4.**One funnel for all returns.** Either always return through `toResult(runtime.x())` (when delegating) or through `errorResult(…)` for the catch arm. Never inline `{ success: false, content: '' }` — `content: ''` collapses the Debug pane to blank.
5.**`debug('lobe-<name>:executor')`.** Match the namespace to the identifier minus `lobe-` when convenient.
6.**Singleton export.**`export const <name>Executor = new <Name>Executor()` — the registry imports the instance, not the class.
### When the executor delegates to ExecutionRuntime
The `toResult` funnel is **mandatory**: it enforces never-undefined `content` and partial-state preservation. Both invariants caught real production bugs (`globLocalFiles` Response empty, `editLocalFile` partial state lost).
---
## 7. `index.ts` — Package Entry Point
Keep it pure data + the manifest. **No React, no stores, no Node-only imports.**
**Why peer not direct deps for client libs:** the `./` and `./executionRuntime` entry points must be importable from server code. Listing React etc. as peer deps prevents bundlers from following them when only the runtime is consumed.
**Skip `./executor`** if the package has no frontend executor (server-only tools like `builtin-tool-web-browsing`).
| "ApiNotFound" at runtime | Method name in executor doesn't match `ApiName` value (typo, wrong case) |
| Method works once, then "this is undefined" | Method declared as `async fn() {}` instead of `fn = async () => {}` — `this` lost when registry invokes |
| Debug "Response" pane blank but `pluginState` populated | Returning `content: ''` or letting `output.content` be undefined — use the `toResult` funnel |
| Partial result vanishes on failure | `toResult` discarded `state` when `success: false`; preserve it |
| Tool shows up but doesn't run on desktop | `executors` in manifest doesn't include `'client'` (or vice versa for server-only) |
| Same tool registered twice / legacy identifier ghost | Identifier collision; check `@deprecated` aliases in `inspectors.ts`/`renders.ts` |
| Manifest test fails after adding API | Forgot to add the corresponding i18n `apiName.<api>` key |
| TypeScript error on `BaseExecutor<typeof X>` | `X` declared with `enum` instead of `as const` object — must be the const-object form |
A builtin tool can ship up to **six client-side surfaces**, each with a different role in the chat UI. Only `Inspector` is required; the other five are added on demand and registered in their own central files.
| Surface | Required? | When the chat shows it | Registered in |
Fall back to `createStyles + token` only when you need runtime token computation (rare). Inline `style={{ color: cssVar.colorTextSecondary }}` is fine for one-off dynamic values.
### 0.3 Use `@lobehub/ui`, not raw `antd`
`Block`, `Text`, `Flexbox`, `Highlighter`, `Alert`, `Tooltip`, `Skeleton` all come from `@lobehub/ui`. Modals come from `@lobehub/ui/base-ui` (`createModal`, `useModalContext`, `confirmModal`) — see the **modal** skill.
Memory note: `@lobehub/ui`'s `<Text type='secondary'>` is a lighter shade than `colorTextSecondary`. If you need that exact token color, write `<Text style={{ color: cssVar.colorTextSecondary }}>`.
### 0.5 Always type with `BuiltinXProps<Args, State>` generics
Don't widen to `any`. The Args generic is the JSON Schema params, the State generic is the executor's `state` field. The two should match `<Name>Params` and `<Name>State` from `types.ts`.
### 0.6 Pull strings from `t('plugin')`
```tsx
const{t}=useTranslation('plugin');
t('builtins.<identifier>.apiName.<api>');
```
Every Inspector should default to `t('builtins.<identifier>.apiName.<api>')` so it shows something while args stream in.
### 0.7 Read store state from `@/store/chat`, not props
Tool surfaces sometimes need cross-cutting state (loading, streaming buffer). Read it inside the component via Zustand selectors, not from props — props only carry args/state/messageId.
---
## 1. Inspector — Header Chip (required)
**Lifecycle:** Inspector renders for **every phase** of a tool call: while args are streaming in, while the executor is running, and after results come back. It's the only surface that's always visible.
**Goal:** keep it to a single line. Show what's happening with as much context as is currently available.
| Args streaming, no useful field yet | `isArgumentsStreaming === true`, `partialArgs.X` undefined | Just the API title with `shinyTextStyles.shinyText` |
| Args streaming, key field arrived | `partialArgs.X` populated | Title + key field chip, still pulse-animated |
| Args complete, executor running | `args` populated, `isLoading === true` | Same as above, still pulse-animated |
| Result arrived | `pluginState` populated, `isLoading === false` | Title + chips + result summary (count, identifier, status) |
- Wrap the whole row with `inspectorTextStyles.root` (provides correct flex / line-height baseline).
- Pulse with `shinyTextStyles.shinyText` whenever `isArgumentsStreaming || isLoading`.
- Show the i18n title first so the row is non-empty during the earliest streaming phase.
- Read both `args?.X` and `partialArgs?.X` together — `args` is final, `partialArgs` is in-stream.
- Use chips/tags for distinct facets (identifier, name, parent, status, count). Each chip should clip with `text-overflow: ellipsis` and have a `max-width` so long values don't blow out the chat bubble.
- Append `pluginState`-derived suffixes only **after** loading finishes — count or "(no results)" should not appear while still searching.
**Lifecycle:** rendered **once the result arrives** (after Placeholder/Streaming hand off). Sits below the Inspector header.
**Skip if** the API is read-only or the result is just text — the framework already shows the executor's `content` string. Add a Render only when there's a structured artifact worth seeing: a card, a chart, a diff, a list of files.
If the Render should hide for certain results (e.g. ClaudeCode's TodoWrite hides when the agent is mid-stream), add a `RenderDisplayControl` to `packages/builtin-tools/src/displayControls.ts`. See `ClaudeCodeRenderDisplayControls` for the pattern.
---
## 3. Placeholder — Skeleton Between Args and Result (optional)
**Lifecycle:** rendered when the args have finished streaming but the executor hasn't returned yet. Disappears when `pluginState` arrives. Bridges the moment of perceived lag.
**Add for** APIs with noticeable execution time: web search, network crawl, file list, large grep. **Skip for** instant ops (status flips, calculator).
- **Mirror the eventual Render's layout.** When the result arrives the Placeholder unmounts and the Render mounts; if they share dimensions, the chat doesn't jump.
- Use `Skeleton.Block` / `Skeleton.Button` from `@lobehub/ui` for placeholder shapes.
- Embed any args you have (e.g. the query text) — context helps the user know what's loading.
- Pulse with `shinyTextStyles.shinyText` if the Placeholder includes literal text.
## 4. Streaming — Live Output During Execution (optional)
**Lifecycle:** rendered **while the executor is still running** for APIs that emit incremental output. The component is responsible for fetching the in-flight stream from the chat store and rendering it.
messageId: string;// use to fetch the streaming buffer from store
toolCallId: string;
}
```
Note there's **no `state` or `result` prop** — the Streaming component is for the in-flight phase. It pulls the live buffer from the store itself (typically via `chatToolSelectors.streamingContent(messageId)` or similar).
**Lifecycle:** rendered **before the executor runs** for APIs whose manifest sets `humanIntervention`. The user sees a preview of the args, can edit them, then approves or skips/cancels.
- **Show a preview, not a form by default.** Editing UI is opt-in via `onArgsChange` and is usually inline (click to edit a code block, etc.).
- For args with debounced edit state (text fields), use `registerBeforeApprove(id, flushFn)` so the approve action waits for the debounce to flush. Always return the cleanup function.
- Call `onInteractionAction({ type: 'submit', payload })` when the user approves; `'skip'` if they skip with a reason; `'cancel'` if they cancel the whole turn.
- Add a corresponding `interventionAudit.ts` in the package root if the tool needs scope/path validation before approval (see `local-system/src/interventionAudit.ts`).
**Lifecycle:** rendered when the user opens the tool message in a side panel or full-screen modal. One Portal per **tool**, not per API — the Portal switches on `apiName` internally.
**Add for** tools whose results deserve a deep-dive view: search results with editable filters, page content with reader mode, code interpreter sessions.
| Portal opens but blank | Switch in `Portal/index.tsx` doesn't cover the apiName | | |
| Strings show as `builtins.lobe-foo.apiName.bar` | Missing i18n key in `src/locales/default/plugin.ts` (or not seeded in dev locale files) | | |
| Wrong color shade on `<Text type="secondary">` | `type='secondary'` is lighter than `colorTextSecondary` — pass via `style={{ color: cssVar.colorTextSecondary }}` | | |
This is a worked example of the canonical 6-step recipe applied to a new entity (`Dataset`), showing a variant of the main skill's pattern: **a list keyed by a parent id** (`datasetMap[benchmarkId]`), useful when the same shape appears under different parents.
If you only need the canonical (single-array) pattern, the main `SKILL.md` already shows it for `Benchmark`. Read this file when you need the parent-keyed Map variant, or when you want a checklist-style walkthrough.
description: Debug package usage guide. Use when adding debug logging, understanding log namespaces, or implementing debugging features. Triggers on debug logging requests or logging implementation.
name: debug-package
description: "Guide for the `debug` npm package and LobeHub log namespaces (lobe-server:*, lobe-desktop:*, lobe-client:*, lobe-*-router:*). Use whenever adding a `debug(...)` logger, picking a namespace for new server/desktop/client/router code, troubleshooting why DEBUG=lobe-* logs don't show up, or when the user asks to 'add logging', 'add a logger', 'instrument this', 'trace this call', 'why isn't my log printing', or mentions `debug(`, `DEBUG=`, `localStorage.debug`, or log format specifiers like %O / %o / %s / %d in a LobeHub codebase."
description: Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.
description: "Drizzle ORM schema authoring and query style for LobeHub (postgres, strict mode). Use when editing anything under `src/database/schemas/`, defining `pgTable` columns/indexes/junction tables, spreading `...timestamps`, generating `createInsertSchema`/`$inferSelect`/`$inferInsert` types, writing `db.select().from(...).leftJoin(...)` queries, or deciding when to split a relational `with:` into two queries. Triggers on `pgTable`, `db.select`, `db.query`, `eq()`/`and()`/`inArray()`, `uniqueIndex`, `primaryKey`, `references({ onDelete })`, 'add a column', 'new table', 'foreign key', 'junction table', 'schema field'. For migration files specifically, see the `db-migrations` skill."
user-invocable: false
---
# Drizzle ORM Schema Style Guide
@@ -125,11 +126,7 @@ The relational API generates complex lateral joins with `json_build_array` that
description: Guide for adding keyboard shortcuts. Use when implementing new hotkeys, registering shortcuts, or working with keyboard interactions. Triggers on hotkey implementation or keyboard shortcut tasks.
description: "Adding or editing keyboard shortcuts in LobeHub. Use when registering a new hotkey, changing a key combo, scoping a shortcut to chat vs global, or wiring a hotkey hook + tooltip. Covers the 5-step flow: add to `HotkeyEnum` in `src/types/hotkey.ts`, register in `HOTKEYS_REGISTRATION` (`src/const/hotkeys.ts`) with `combineKeys([Key.Mod, …])`, add i18n in `src/locales/default/hotkey.ts`, expose via `useHotkeyById` in `src/hooks/useHotkeys/`, and render `<Tooltip hotkey={…}>`. Triggers on `HotkeyEnum`, `HOTKEYS_REGISTRATION`, `useHotkeyById`, `combineKeys`, `Key.Mod`/`Key.Shift`, 'add a hotkey', 'add a shortcut', '加快捷键', '快捷键', 'Cmd+K', 'keyboard shortcut', 'hotkey scope', 'hotkey conflict'."
description: Internationalization guide using react-i18next. Use when adding translations, creating i18n keys, or working with localized text in React components (.tsx files). Triggers on translation tasks, locale management, or i18n implementation.
description: "LobeHub internationalization with react-i18next. Use when adding any user-facing string in `.tsx`/`.ts` files, creating or renaming a key under `src/locales/default/{namespace}.ts`, deciding the `{feature}.{context}.{action}` flat-key pattern, wiring a new namespace into `src/locales/default/index.ts`, or translating zh-CN/en-US JSON for dev preview. Triggers on `useTranslation`, `t('foo.bar')`, `i18next.t`, `{{variable}}` interpolation, hardcoded UI strings (zh or en) that should be extracted, 'add i18n', '加 i18n key', '翻译', 'locale key', 'namespace', 'pnpm i18n'."
description: "Linear issue management. MUST USE when: (1) user mentions LOBE-xxx issue IDs (e.g. LOBE-4540), (2) user says 'linear', 'linear issue', 'link linear', (3) creating PRs that reference Linear issues. Provides workflows for retrieving issues, updating status, and adding comments."
description: "Linear issue management. Use when the user mentions LOBE-xxx issue IDs (e.g. LOBE-4540), says 'linear' / 'linear issue' / 'link linear', or when creating PRs that reference Linear issues. Covers retrieving issues, updating status, adding completion comments, and creating sub-issue trees."
user-invocable: false
---
# Linear Issue Management
Before using Linear workflows, search for `linear` MCP tools. If not found, treat as not installed.
## ⚠️ CRITICAL: PR Creation with Linear Issues
## PR Creation with Linear Issues
**When creating a PR that references Linear issues (LOBE-xxx), you MUST:**
A PR that fixes a Linear issue has **two separate jobs to do**, and both matter:
1.Create the PR with magic keywords (`Fixes LOBE-xxx`)
2.**IMMEDIATELY after PR creation**, add completion comments to ALL referenced Linear issues
3. Do NOT consider the task complete until Linear comments are added
1.**`Fixes LOBE-xxx` in the PR body** — Linear watches GitHub for these magic keywords and auto-links the PR and auto-closes the issue on merge. This is the machine-readable side.
2.**A completion comment on the Linear issue** — gives the reviewer/PM/teammate landing in Linear a human-readable summary of what changed and why, without forcing them to click through to GitHub and read a diff.
This is NON-NEGOTIABLE. Skipping Linear comments is a workflow violation.
If you only do step 1, Linear watchers (often non-engineers) hit the issue and see no context. So pair PR creation with the Linear comment as part of the same task — finish both before considering the work done.
## Workflow
1.**Retrieve issue details** before starting: `mcp__linear-server__get_issue`
2.**Read images**: If the issue description contains images, MUST use `mcp__linear-server__extract_images` to read image content for full context
3.**Check for sub-issues**: Use`mcp__linear-server__list_issues` with `parentId` filter
4.**Mark as In Progress**: When starting to plan or implement an issue, immediately update status to **"In Progress"** via `mcp__linear-server__update_issue`
2.**Read images** — issue descriptions often contain screenshots with critical context (mockups, error states, before/after). Use `mcp__linear-server__extract_images` so you actually see them; reading raw markdown alone misses what the reporter was looking at.
3.**Check for sub-issues**: `mcp__linear-server__list_issues` with `parentId` filter
4.**Mark as In Progress** at the moment you start planning or implementing — this signals to teammates the issue is owned, so they don't double-pick it up.
5.**Update issue status** when completing: `mcp__linear-server__update_issue`
6.**Add completion comment** (see [format below](#completion-comment-format))
## Creating Issues
When creating issues with `mcp__linear-server__create_issue`,**MUST add the `claude code` label**.
When creating issues with `mcp__linear-server__create_issue`, add the `claude code` label. Reason: the label is how the team filters/audits AI-generated issues; without it those issues vanish into the general backlog and the team loses visibility into AI contribution patterns.
## Language
Match the issue language to the conversation that produced it — if you're discussing in 中文,write the issue in 中文;if discussing in English, write it in English. Reason: the issue is a continuation of the conversation, and forcing a language switch creates translation friction for the collaborator who started the thread.
- Code blocks, file paths, and quoted strings always stay in their original form regardless of surrounding language.
- This applies equally to **updates** — when editing an existing issue (description **and titles**), preserve the language of the conversation that triggered the edit; don't switch the issue language mid-refactor.
## 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
### 1. 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.
The Linear Sub-issues panel orders children by `sortOrder`, which **defaults to newest-first** (most recently created appears on top). Neither parallel nor serial creation produces the intended top-to-bottom reading order, and the MCP `save_issue` tool does **not expose a `sortOrder` parameter** — you can't set order at create time.
**Workaround**: encode execution order in the title itself:
Workaround: encode execution order in the title itself:
```plaintext
[1] [db] add schema fields
@@ -89,7 +100,7 @@ The implementer may open only the sub-issue, not the parent — don't rely on co
## Completion Comment Format
Every completed issue MUST have a comment summarizing work done:
Each completed issue gets a comment summarizing the work, so reviewers and future readers don't have to reconstruct it from the PR diff:
```markdown
## Changes Summary
@@ -105,34 +116,28 @@ Every completed issue MUST have a comment summarizing work done:
- ...
```
This is critical for:
This gives team visibility, code-review context, and a paper trail for future reference.
- Team visibility
- Code review context
- Future reference
## PR Association
## PR Association (REQUIRED)
When creating PRs for Linear issues, include magic keywords in PR body:
When creating PRs for Linear issues, include magic keywords in the PR body:
-`Fixes LOBE-123`
-`Closes LOBE-123`
-`Resolves LOBE-123`
These trigger Linear's auto-link + auto-close on merge.
## Per-Issue Completion Rule
When working on multiple issues, update EACH issue IMMEDIATELY after completing it:
When working on multiple issues, close out **each one before starting the next** — don't batch all the Linear updates to the end. Batching is where comments get forgotten and issues stay stuck in "In Progress" days after the PR shipped.
For each issue:
1. Complete implementation
2. Run `bun run type-check`
3. Run related tests
4. Create PR if needed
5. Update status to **"In Review"** (NOT "Done")
6.**Add completion comment immediately**
7. Move to next issue
**Note:** Status → "In Review" when PR created. "Done" only after PR merged.
**❌ Wrong:** Complete all → Create PR → Forget Linear comments
description: UI copy and microcopy guidelines. Use when writing UI text, buttons, error messages, empty states, onboarding, or any user-facing copy. Triggers on i18n translation, UI text writing, or copy improvement tasks. Supports both Chinese and English.
user-invocable: false
---
# LobeHub UI Microcopy Guidelines
This file is the quick-reference summary. For full prompt-style guidelines with extensive examples (anti-patterns, tone matrices, scenario walk-throughs), load the language-specific reference:
description: MUST use when creating, editing, or writing modaldialogs 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.
description: "LobeHub imperative-modal conventions. Use whenever creating, editing, opening, or migrating a modal/dialog/popup — prefer `createModal` / `confirmModal` / `useModalContext` from `@lobehub/ui/base-ui` (headless) over the legacy root `@lobehub/ui``createModal` (antd Modal props) and over any declarative `open` state + `<Modal />` pattern. Covers required `ModalHost` mounting, the `Content` + `index.tsx` file layout, `content` vs `children` slot, i18n inside `createModal()` (`import { t } from 'i18next'`), and migration notes. Triggers on `createModal`, `confirmModal`, `useModalContext`, `ModalHost`, `antd Modal`, `<Modal open>`, 'open a modal', 'popup', 'dialog', 'confirm dialog', '弹框', '弹窗', '确认框', 'migrate to base-ui'."
description: Complete project architecture and structure guide. Use when exploring the codebase, understanding project organization, finding files, or needing comprehensive architectural context. Triggers on architecture questions, directory navigation, or project overview needs.
description: React component development guide. Use when working with React components (.tsx files), creating UI, using @lobehub/ui components, implementing routing, or building frontend features. Triggers on React component creation, modification, layout implementation, or navigation tasks.
description: "LobeHub React/SPA component conventions: antd-style with `createStaticStyles` + `cssVar.*` (prefer zero-runtime over `createStyles` + `token`), `@lobehub/ui/base-ui` primitives before `@lobehub/ui` before antd, `Flexbox`/`Center` for layouts, react-router-dom navigation, and the `.desktop.tsx` sync rule. Use when writing or editing any `.tsx` under `src/**`, picking a styling helper, choosing a component (Select/Modal/Drawer/Button/Tooltip), wiring routes in `desktopRouter.config.tsx`/`.desktop.tsx`, or adding a `Link`/`useNavigate` call in the SPA. Triggers on `createStyles`/`createStaticStyles`, `cssVar`, `@lobehub/ui`, `antd-style`, `Flexbox`, `useNavigate`, `react-router-dom`, `Link`, 'new component', 'add a page', 'edit a layout', 'desktopRouter', 'componentMap.desktop'."
Use this skill when the user asks to run the migrated source command `dedupe`.
## Command Template
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
description: MUST use when editing src/routes/ segments, src/spa/router/desktopRouter.config.tsx or desktopRouter.config.desktop.tsx (always change both together), mobileRouter.config.tsx, or when moving UI/logic between routes and src/features/.
| `desktopRouter.config.tsx` | Dynamic imports via `dynamicElement` / `dynamicLayout` — code-splitting; used by `entry.web.tsx` and `entry.desktop.tsx`. |
| `desktopRouter.config.desktop.tsx` | Same route tree with **synchronous** imports — kept for Electron / local parity and predictable bundling. |
| `desktopRouter.config.tsx` | Dynamic imports via `dynamicElement` / `dynamicLayout` — code-splitting; used by `entry.web.tsx` and `entry.desktop.tsx`. |
| `desktopRouter.config.desktop.tsx` | Same route tree with **synchronous** imports — kept for Electron / local parity and predictable bundling. |
Anything that changes the tree (new segment, renamed `path`, moved layout, new child route) must be reflected in **both** files in one PR or commit. Remove routes from both when deleting.
description: Zustand store data structure patterns for LobeHub. Covers List vs Detail data structures, Map + Reducer patterns, type definitions, and when to use each pattern. Use when designing store state, choosing data structures, or implementing list/detail pages.
user-invocable: false
---
# LobeHub Store Data Structures
This guide covers how to structure data in Zustand stores for optimal performance and user experience.
How to structure data in Zustand stores for fast list rendering, multi-detail caching, and ergonomic optimistic updates.
## Core Principles
### ✅ DO
1.**Separate List and Detail**- Use different structures for list pages and detail pages
2.**Use Map for Details**- Cache multiple detail pages with `Record<string, Detail>`
3.**Use Array for Lists**- Simple arrays for list display
4.**Types from @lobechat/types**- Never use `@lobechat/database` types in stores
5.**Distinguish List and Detail types**- List types may have computed UI fields
1.**Separate List and Detail**— different structures for list pages and detail pages
2.**Use Map for Details**— cache multiple detail pages with `Record<string, Detail>`
3.**Use Array for Lists**— simple arrays for list display
4.**Types from `@lobechat/types`**— never use `@lobechat/database` types in stores
5.**Distinguish List and Detail types**— List types may have computed UI fields
### ❌ DON'T
1.**Don't use single detail object**- Can't cache multiple pages
2.**Don't mix List and Detail types**- They have different purposes
3.**Don't use database types**- Use types from `@lobechat/types`
4.**Don't use Map for lists**- Simple arrays are sufficient
1.**Don't use a single detail object**— can't cache multiple pages
2.**Don't mix List and Detail types**— they have different purposes
3.**Don't use database types**— use types from `@lobechat/types`
4.**Don't use Map for lists**— simple arrays are sufficient
---
## Type Definitions
Types should be organized by entity in separate files:
Each entity gets its own file under `@lobechat/types/`. Each file exports two types:
```
@lobechat/types/src/eval/
├── benchmark.ts # Benchmark types
├── agentEvalDataset.ts # Dataset types
├── agentEvalRun.ts # Run types
└── index.ts # Re-exports
```
- **Detail type** — full entity, including heavy fields (rubrics, content, editor state, …)
- **List item type** — a **subset** that excludes heavy fields, may add computed UI fields (counts, timestamps formatted for display)
### Example: Benchmark Types
**Important:** the List type is a **subset**, not an `extends` of Detail. Extending pulls the heavy fields right back in.
```typescript
// packages/types/src/eval/benchmark.ts
importtype{EvalBenchmarkRubric}from'./rubric';
// ============================================
// Detail Type - Full entity (for detail pages)
// ============================================
/**
* Full benchmark entity with all fields including heavy data
*/
exportinterfaceAgentEvalBenchmark{
createdAt: Date;
description?: string|null;
id: string;
identifier: string;
isSystem: boolean;
metadata?: Record<string,unknown>|null;
name: string;
referenceUrl?: string|null;
rubrics: EvalBenchmarkRubric[];// Heavy field
updatedAt: Date;
}
// ============================================
// List Type - Lightweight (for list display)
// ============================================
/**
* Lightweight benchmark item - excludes heavy fields
* May include computed statistics for UI
*/
exportinterfaceAgentEvalBenchmarkListItem{
createdAt: Date;
description?: string|null;
id: string;
identifier: string;
isSystem: boolean;
name: string;
// Note: rubrics NOT included (heavy field)
// Computed statistics for UI display
datasetCount?: number;
runCount?: number;
testCaseCount?: number;
}
```
### Example: Document Types (with heavy content)
```typescript
// packages/types/src/document.ts
/**
* Full document entity - includes heavy content fields
*/
exportinterfaceDocument{
id: string;
title: string;
description?: string;
content: string;// Heavy field - full markdown content
editorData: any;// Heavy field - editor state
metadata?: Record<string,unknown>;
createdAt: Date;
updatedAt: Date;
}
/**
* Lightweight document item - excludes heavy content
*/
exportinterfaceDocumentListItem{
id: string;
title: string;
description?: string;
// Note: content and editorData NOT included
createdAt: Date;
updatedAt: Date;
// Computed statistics
wordCount?: number;
lastEditedBy?: string;
}
```
**Key Points:**
- **Detail types** include ALL fields from database (full entity)
- **List types** are **subsets** that exclude heavy/large fields
- List types may add computed statistics for UI (e.g., `testCaseCount`)
- **Each entity gets its own file** (not mixed together)
- **All types** exported from `@lobechat/types`, NOT `@lobechat/database`
**Heavy fields to exclude from List:**
- Large text content (`content`, `editorData`, `fullDescription`)
When the Detail Map needs optimistic updates (i.e. the user edits a row and the UI should reflect it before the server confirms), wire a typed reducer instead of inlining `set` calls. This keeps mutations testable and the dispatch surface small.
- **Immutable updates** - Immer ensures immutability
> See [`references/reducer.md`](./references/reducer.md) for the full discriminated-union action types, the `produce`-based reducer, and the `internal_dispatch*` slice methods that connect them to Zustand.
---
## Data Structure Comparison
### ❌ WRONG - Single Detail Object
### ❌ WRONG — Single Detail Object
```typescript
interfaceBenchmarkSliceState{
// ❌ Can only cache one detail
benchmarkDetail: AgentEvalBenchmark|null;
// ❌ Global loading state
isLoadingBenchmarkDetail: boolean;
}
```
**Problems:**
Problems:
- Can only cache one detail page at a time
- Switching between details causes unnecessary refetches
The `internal_` prefix is a convention — UI components should call the public mutation methods (e.g. `updateBenchmark`), which in turn call `internal_dispatch*`. This keeps reducer dispatch shapes out of the component layer.
The reason these belong only on Detail: list pages render many rows, so pulling heavy fields blows up payload size and slows render. Detail pages render one entity, so the full payload is fine.
description: Testing guide using Vitest. Use when writing tests (.test.ts, .test.tsx), fixing failing tests, improving test coverage, or debugging test issues. Triggers on test creation, test debugging, mock setup, or test-related questions.
description: TRPC router development guide. Use when creating or modifying TRPC routers (src/server/routers/**), adding procedures, or working with server-side API endpoints. Triggers on TRPC router creation, procedure implementation, or API endpoint tasks.
description: TypeScript code style and optimization guidelines. MUST READ before writing or modifying any TypeScript code (.ts, .tsx, .mts files). Also use when reviewing code quality or implementing type-safe patterns. Triggers on any TypeScript file edit, code style discussions, or type safety questions.
description: "TypeScript code style and type-safety guide for LobeHub. Read before writing or editing any `.ts` / `.tsx` / `.mts` — covers `interface` vs `type`, `Record<PropertyKey, unknown>` over `any`/`object`, `as const satisfies`, `@ts-expect-error` over `@ts-ignore`, `import type` (`separate-type-imports`), `async`/`await` + `Promise.all`, `for…of` over indexed `for`, and the no-silent-`.catch(() => fallback)` rule. Also use when reviewing type quality, deciding module augmentation (`declare module`) over `namespace`, or designing extensible types (e.g. `PipelineContext.metadata`). Triggers on any TypeScript file edit, 'fix the type', 'why is this `any`', 'should this be interface or type', 'eslint type-import', 'ts-expect-error'."
user-invocable: false
---
# TypeScript Code Style Guide
@@ -28,12 +29,16 @@ description: TypeScript code style and optimization guidelines. MUST READ before
## Imports
- This project uses `simple-import-sort/imports` and `consistent-type-imports` (`fixStyle: 'separate-type-imports'`)
- **Separate type imports**: always use `import type { ... }` for type-only imports, NOT `import { type ... }` inline syntax
- When a file already has `import type { ... }` from a package and you need to add a value import, keep them as **two separate statements**:
```ts
import type { ChatTopicBotContext } from '@lobechat/types';
import { RequestTrigger } from '@lobechat/types';
```
- Within each import statement, specifiers are sorted **alphabetically by name**
## Code Structure
@@ -42,6 +47,7 @@ description: TypeScript code style and optimization guidelines. MUST READ before
- Use consistent, descriptive naming; avoid obscure abbreviations
- Replace magic numbers/strings with well-named constants
- Defer formatting to tooling
- Prefer **named exports** over `export default` — keeps refactor renames and IDE auto-import in sync, and avoids the `default` re-naming drift you get with `import Foo from './foo'`. Reserve `export default` for files where the framework requires it (Next.js page/route/layout, React.lazy targets, config files like `vitest.config.ts`)
## UI and Theming
@@ -51,7 +57,6 @@ description: TypeScript code style and optimization guidelines. MUST READ before
## Performance
- Prefer `for…of` loops over index-based `for` loops
- Reuse existing utils in `packages/utils` or installed npm packages
Two real workflows already in the codebase that follow this skill's pattern verbatim. Skim them when you want to see the pattern applied to concrete entities.
## Example 1: Welcome Placeholder
**Use case:** Generate AI-powered welcome placeholders for users.
**Structure:**
- Layer 1: `process-users` — entry point, checks eligible users
- Layer 2: `paginate-users` — paginates through active users
- Layer 3: `generate-user` — generates placeholders for ONE user
**Key features:**
- Filters users who already have cached placeholders in Redis
-`paidOnly` flag to scope to subscribed users
-`dryRun` mode for statistics
- Fan-out for large user batches (`CHUNK_SIZE=20`)
Both workflows are the **same pattern** — they only differ in:
- Entity type (users vs agents)
- Business logic (placeholder generation vs welcome generation)
- Data source (different database queries)
Everything else — the 3-layer split, dry-run handling, fan-out, filter-existing, flowControl tuning — is identical. That's the whole point: once you internalize the pattern, adding a new workflow is mostly entity-substitution.
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)."
disable-model-invocation: true
argument-hint: '[minor|patch] [version?]'
---
# Version Release Workflow
This skill is a router. The detailed steps live in `references/`.
## Scope Boundary (Important)
This skill is only for:
@@ -28,68 +32,12 @@ The primary development branch is **canary**. All day-to-day development happens
Only two release types are used in practice (major releases are extremely rare and can be ignored):
| Type | Use Case | Frequency | Source Branch | PR Title Format | Version |
| Minor | Feature iteration release | \~Every 4 weeks | canary | `🚀 release: v{x.y.0}` | Manually set |
| Patch | Weekly release / hotfix / model / DB migration | \~Weekly or as needed | canary or main | Custom (e.g. `🚀 release: 20260222`) | Auto patch +1 |
| Type | Use Case | Frequency | Source Branch | PR Title Format | Version | Reference |
| Patch | Weekly release / hotfix / model / DB migration | \~Weekly or as needed | canary or main | Custom (e.g. `🚀 release: 20260222`) | Auto patch +1 |`references/patch-release-scenarios.md` |
## Minor Release Workflow
Used to publish a new minor version (e.g. `v2.2.0`), roughly every 4 weeks.
### Steps
1.**Create a release branch from canary**
```bash
git checkout canary
git pull origin canary
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)
3.**Create a PR to main**
```bash
gh pr create \
--title "🚀 release: v{version}"\
--base main \
--head release/v{version}\
--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.
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
```bash
bun run release:branch # Interactive
bun run release:branch --minor # Directly specify minor
```
## Patch Release Workflow
Version number is automatically bumped by patch +1. There are 4 common scenarios:
| 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 |
All scenarios auto-bump patch +1. Patch PR titles do not need a version number. See `reference/patch-release-scenarios.md` for detailed steps per scenario.
### Scripts
```bash
bun run hotfix:branch # Hotfix scenario
```
For writing the release-note body (any release type), see `references/release-notes-style.md`.
- **Patch release** (weekly / hotfix / model launch / DB migration) → `references/patch-release-scenarios.md`
- **Writing the PR body / release notes** (any release type) → `references/release-notes-style.md`
Choose workflow by scenario (see `reference/patch-release-scenarios.md`):
### Hard Rules (apply to every release type)
- **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
### Hard Rules
- **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
## GitHub Release Changelog Standard (Long-Form Style)
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.
### Positioning
This release-note style is:
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)
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.
### Contributor Ordering
Render contributors as a **single flat list** (no separate "Community" / "Core Team" subsections). Order: **community contributors first, team members after**. Within each group, sort by PR count desc. Bots (`@lobehubbot`, `renovate[bot]`) go on a separate "maintenance" line.
**LobeHub team roster** — anyone in this list is a team member; anyone not in this list is a community contributor:
-@arvinxx
-@Innei
-@tjx666 (commit author name: YuTengjing)
-@LiJian
-@Neko
-@Rdmclin2
-@AmAzing129
-@sudongyuer
-@rivertwilight
-@CanisMinor
> **Resolving handles** — git author names (e.g. `YuTengjing`) are not always the GitHub handle. Verify via `gh pr view <PR> --json author` or `gh api search/users -f q='<email>'` before listing.
If a new contributor appears who is not on this list, treat them as community by default and ask the user whether to add them to the roster.
- [ ] 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
- **Do NOT** manually modify `package.json` version — CI handles it.
- **Do NOT** manually create tags — CI handles them.
-Minor PR title format is strict (`🚀 release: v{x.y.z}`).
-Patch PRs do not need an explicit version number.
- Keep release facts accurate; do not invent metrics or availability statements. Release-note inputs (compare base, PR refs, contributor list) **must be derived from `git`** per `references/release-notes-style.md` § Computing Inputs — never from memory or descriptions.
**Migration Scope:** Agent benchmark data model bootstrap (5 new tables, 2 new indexes)
@@ -7,14 +7,6 @@
---
## ✨ Highlights
- **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.
> 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.
Used to publish a new minor version (e.g. `v2.2.0`), roughly every 4 weeks. The PR title carries the exact version number; CI parses it to drive the rest of the release.
## Steps
1.**Create a release branch from canary**
```bash
git checkout canary
git pull origin canary
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`).
3. **Create a PR to main**
```bash
gh pr create \
--title "🚀 release: v{version}" \
--base main \
--head release/v{version} \
--body-file release_body.md
```
> \[!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. **Write the PR body as release notes** — Follow `release-notes-style.md`. Compare base is the latest semver tag on main (`git describe --tags --abbrev=0 origin/main`).
5. **Automatic trigger after merge** — `auto-tag-release` detects the title format, uses the version number from the title, bumps `package.json`, tags `v{x.y.z}`, creates the GitHub Release, and dispatches `sync-main-to-canary`.
## Scripts
```bash
bun run release:branch # Interactive
bun run release:branch --minor # Directly specify minor
```
## Hard Rules (specific to Minor)
- PR title format is **strict**: `🚀 release: v{x.y.z}`. Any deviation falls through to patch detection.
- Do **NOT** manually modify `package.json` version — CI will bump it.
- Do **NOT** manually create the tag — CI will tag.
- Highlights bullet count is usually 8–12 (see `release-notes-style.md` size heuristics).
Write a user-facing changelog following the format in `patch-release-changelog-example.md`.
Then follow `./release-notes-style.md` § **Computing Inputs (Hard Rules)** to derive PR refs, metrics, and contributors. Every `(#XXXX)`in the body must come from actual commit subjects in this range — never inferred from descriptions.
3. **Create PR to main** with the changelog as the PR body
# GitHub Release Changelog Standard (Long-Form Style)
Use this guide for **GitHub Release notes** — the body of a release PR that becomes the GitHub Release after merge. Do **not** use it for `docs/changelog/*.mdx` website pages (load `../../docs-changelog/SKILL.md` instead).
## Table of Contents
1. [Positioning](#positioning) — what this style optimizes for
2. [Required Inputs Before Writing](#required-inputs-before-writing)
3. [Computing Inputs (Hard Rules — Verify, Never Guess)](#computing-inputs-hard-rules--verify-never-guess) — base ref, PR refs, metrics, authors, pre-publish verification
4. [Canonical Structure (Long-Form: Minor / Weekly)](#canonical-structure-long-form-minor--weekly)
5. [Variants for Shorter Releases](#variants-for-shorter-releases) — hotfix, DB migration
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.
## Computing Inputs (Hard Rules — Verify, Never Guess)
> Hallucinated PR numbers and wrong "Since v..." bases are the #1 failure mode of this skill. Every number and every `(#XXXX)` must come from `git`, never from memory or inference.
### 1. Compare base = latest semver tag on `main`
Do **not** eyeball the tag list or pick the "last weekly" PR. Compute it:
Sanity check that the tag is reachable from the release branch:
```bash
git merge-base --is-ancestor "$PREV_TAG" origin/release/weekly-{YYYYMMDD}&&echo OK
```
If the check fails, stop and ask the user — the release branch is based on the wrong source.
> **Why not "the last weekly release PR"?** Hotfixes (`v2.1.54`, `v2.1.55`, …) merge directly into main between weeklies. They get back-merged via `sync-main-to-canary`, so the latest semver tag on main _is_ the correct previous release for both weekly and minor flows. Picking the previous weekly's tag will silently undercount and put a stale version in "Since v…".
### 2. PR refs must come from commit subjects — never from descriptions
- Every `(#XXXX)` you write in the body **must** appear in `/tmp/release_prs.txt`. No exceptions.
- Never infer a PR number from a feature description. If you remember "the KB BM25 PR was around #14501", that memory is wrong about half the time. Look up the commit hash by feature keyword and read its actual subject.
- If your terminal truncates long subjects (any wrapper that compresses output, e.g. `rtk`), bypass it. With `rtk` use `rtk proxy git log …`. Verify with `wc -l /tmp/release_prs.txt` — the count must match `git log $PREV_TAG..HEAD --no-merges --pretty=format:'%h' | wc -l` minus the few commits without a PR ref. A mismatch of >5% means subjects are being silently truncated.
sed 's/[()]//g' /tmp/release_prs.txt > /tmp/release_prs_clean.txt
echo"=== In body but NOT in actual range (must be EMPTY) ==="
comm -23 /tmp/body_prs.txt /tmp/release_prs_clean.txt
```
Empty diff = OK. Any output = the body cites a PR that wasn't merged in this range. Stop and fix before publishing.
Also verify the metrics line in the body matches the computed values (`PR_COUNT`, `CONTRIBUTOR_COUNT`) and that `**Full Changelog**` uses `$PREV_TAG`, not some older tag.
## Canonical Structure (Long-Form: Minor / Weekly)
Follow this section order for **Minor** and **Weekly** releases unless the user asks otherwise. For **Hotfix** and **DB Migration**, see § Variants for Shorter Releases below — the canonical structure does not apply.
1.`# 🚀 LobeHub Release (<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)
Use `---` separators between major blocks for long releases.
## Variants for Shorter Releases
The Canonical Structure above is for **long-form** (Minor / Weekly). Two short-form variants override it.
### Hotfix Variant
A hotfix targets one regression and ships fast. The body is short and operator-focused — no Highlights, no domain blocks, no Contributors line.
Required sections, in order:
1.`# 🚀 LobeHub Release (<YYYYMMDD>)`
2.`**Hotfix Scope:**` — one line summarizing the regression scope (e.g. `Agent topic-switching regression — stale chat state on agent change`). Replaces the long-form `Release Date` / `Since vX.Y.Z` metrics.
3. One quoted thesis (single paragraph, 1-2 lines) describing what is now restored.
4.`## 🐛 What's Fixed` — 1-3 bullets, each `**<symptom>** — <fix in one sentence>. (#PR)`. No root-cause prose; that lives in the commit message.
5.`## ⚙️ Upgrade` — short notes for self-hosted (pull image / restart, schema or env changes) and cloud (usually "applied automatically").
6.`## 👥 Owner` — single `@handle` for the PR author, resolved via `gh pr view "$PR" --json author --jq '.author.login'`. Never hardcoded.
Hard rules specific to hotfix:
- **No Highlights / domain blocks / Contributors / Full Changelog** — these add noise to a one-shot fix.
- **No metric line** — `Since vX.Y.Z` doesn't apply; the body cites the single PR (or 1-3 PRs) directly.
- **Owner ≠ Contributors** — one author, listed under § Owner. Not a flat handle list.
- See `changelog-example/hotfix.md` for the canonical template.
### DB Migration Variant
Database schema changes that need to be released independently. Operator impact is the headline.
Required sections, in order:
1.`# 🚀 LobeHub Release (<YYYYMMDD>)` + scope line
2.**Migration overview** — what tables / columns are added, modified, or removed
3.**Operator impact** — backwards-compatible? required actions for self-hosted?
4.**Rollback / backup note** — how to recover
5.`## 👥 Owner` — single PR author, resolved via `gh pr view`
See `changelog-example/db-migration.md` for the canonical template.
## 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**
- Long-form structure with multiple domain blocks.
-`Highlights` usually 8-12 bullets.
- **Weekly patch release**
- Long-form skeleton with reduced subsection count.
-`Highlights` usually 4-8 bullets.
- **Hotfix release**
- Short-form (see § Variants → Hotfix). No Highlights, no domain blocks, no Contributors.
- 1-3 fix bullets. Body should fit on one screen.
- **DB migration release**
- Short-form (see § Variants → DB Migration).
- Must include `Migration overview`, operator impact, and rollback/backup note.
## Contributor Ordering
Render contributors as a **single flat list** (no separate "Community" / "Core Team" subsections). Order: **community contributors first, team members after**. Within each group, sort by PR count desc. Bots (`@lobehubbot`, `renovate[bot]`) go on a separate "maintenance" line.
**LobeHub team roster** — anyone in this list is a team member; anyone not in this list is a community contributor:
-@arvinxx
-@Innei
-@tjx666 (commit author name: YuTengjing)
-@LiJian
-@Neko
-@Rdmclin2
-@AmAzing129
-@sudongyuer (commit author name: Tsuki)
-@rivertwilight (commit author name: René Wang)
-@CanisMinor
-@cy948 (commit author name: Rylan Cai)
> **Resolving handles** — git author names (e.g. `YuTengjing`) are not always the GitHub handle. Verify via `gh pr view "$PR" --json author` or `gh api search/users -f q='<email>'` before listing.
If a new contributor appears who is not on this list, treat them as community by default and ask the user whether to add them to the roster.
description: Zustand state management guide. Use when working with store code (src/store/**), implementing actions, managing state, or creating slices. Triggers on Zustand store development, state management questions, or action implementation.
description: "LobeHub Zustand store conventions: public/internal/dispatch action layers, optimistic update pattern, slice composition via `flattenActions`, and class-based action migration. Use whenever working under `src/store/**`, adding a `createXxxSlice`, writing `internal_*` or `internal_dispatch*` actions, designing `messagesMap`/`topicsMap` reducers, refactoring a `StateCreator` object slice into a `XxxActionImpl` class, or debugging stale store reads. Triggers on `useChatStore`/`useUserStore`/`useGlobalStore`, `createStore`, `flattenActions`, `StoreSetter`, `internal_dispatch`, 'add an action', 'zustand selector', 'store slice', 'class action', 'optimistic update'."
- **misc**: clear stale topic when switching agents from a topic route.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: clear stale topic when switching agents from a topic route, closes [#14231](https://github.com/lobehub/lobe-chat/issues/14231) ([deeb97a](https://github.com/lobehub/lobe-chat/commit/deeb97a))
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.