mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
fix/topic-switch-message-refresh
188 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c3a7642295 |
✨ feat(oauth-apps): support web applications with redirect URIs (#19504)
* ✨ feat(oauth-apps): support web applications with redirect URIs OAuth apps created from the settings page were device-flow only: no redirect URIs, no response types, and no client secret. That covers CLIs but leaves no way to register a server-side app that signs users in with a LobeHub account through the authorization code flow. Adds a `web` application type alongside the existing device type. A web app gets `authorization_code` + `refresh_token` grants, a `code` response type, the redirect URIs its owner registers, and a confidential client secret issued once at creation with a rotate action afterwards. The secret is encrypted at rest with `KeyVaultsGateKeeper`, mirroring how API keys are stored, and decrypted in the OIDC adapter because the token endpoint compares it in plaintext. A secret that fails to decrypt is dropped rather than passed through, so the request fails as `invalid_client` instead of making ciphertext the credential. Redirect URIs are matched exactly: no wildcards, no fragments, no embedded credentials, and https everywhere except loopback hosts. The same rules run in the form and again in the tRPC input, since only the server-side pass is binding. No migration: `oidc_clients` already carries every column this needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(oauth-apps): keep the adapter's secret round-trip symmetric Two follow-ups from review. `Client.upsert` still wrote the secret in plaintext while `find` now expects ciphertext, so anything persisting a client through the adapter would read back a credential it could not authenticate with. Encrypt on the way in too, and leave the stored secret untouched when an update payload omits it. The conflict branch also referenced a camelCase key that never existed on the payload, so it silently wrote nothing. An `update` carrying an empty redirect URI list passed validation and stripped a web app of every callback, contradicting the create-time requirement. Reject a list that normalizes to empty wherever one is supplied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(oauth-apps): address acceptance review on create, detail and consent flows Application type is now picked from two icon cards that explain each kind, instead of a bare segmented toggle. Redirect URIs move out of the create dialog and are configured on the app page afterwards; the type stays at creation because it decides whether a client secret is issued and cannot change once the app exists. A freshly created web app shows that it has no redirect URIs yet and cannot sign anyone in until one is added. The detail page separates reading from editing. Basic info and redirect URIs each render read-only with an Edit action, and open a form with Save and Cancel only on request. Redirect URIs are edited as a list of rows rather than a newline-separated textarea. Authorization errors the provider cannot redirect back with, such as an unregistered redirect URI, now land on a dedicated page with a plain explanation per error code instead of a raw JSON dump. Only `error` and `error_description` are forwarded. The consent and sign-in confirmation cards center their title and subtitle so the text lines up with the centered application logo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♿ fix(oauth-apps): label redirect URI row removal and color the list error The icon-only remove button on each redirect URI row had no accessible name, so assistive tech announced it as an unlabeled button. The "at least one redirect URI" message rendered in body color: antd's ErrorList only picks up error styling inside a Form.Item with an error status, and the list-level message sits outside one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
021bd4b4b3 |
🐛 fix(editor): rehost pasted document images (#19527)
* 🐛 fix(editor): rehost pasted document images * 🐛 fix(server): cap remote image fetch size * 🐛 fix(editor): handle rehosted image URLs * ✅ test: type happy-dom URL helper |
||
|
|
fc346bc4cb |
🐛 fix(cli): print wrapped artifact share URL after publish (#19536)
* 🐛 fix(cli): print wrapped artifact share URL after publish CLI artifact publish was emitting the raw hosting publicUrl. Match the App by returning the product share link /share/artifact/<deploymentId>. * ♻️ refactor: drop src/utils/artifactShare re-export Callers import artifactShareUrl from @lobechat/utils/artifactShare. |
||
|
|
515f3b8461 |
🐛 fix(task-slug): keep combining marks and fall back to the instruction (#19519)
* 🐛 fix(task-slug): keep combining marks and fall back to the instruction `taskTitleSlug` only allowed `\p{L}` and `\p{N}`, so scripts built on combining marks were shredded one character at a time — `कार्य पूरा करें` became `क-र-य-प-र-कर`, `สวัสดี` became `สว-สด`. CJK needs no marks, which is why every existing case read as correct. Add `\p{M}`, and truncate by grapheme cluster so a cut can neither halve a surrogate pair nor strand a mark without its base letter. The Recent feed links a task by `COALESCE(name, instruction)` while `useCanonicalTaskSlug` read `name` alone, so a task with only an instruction had its slug stripped the moment the detail landed, and the copied link came out bare. Add a shared `taskSlugTitle` helper mirroring that COALESCE (`??`, so a name cleared to `''` still collapses the URL) and read it through store selectors everywhere a task link is built or repaired. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🔒 fix(task-slug): keep prompt bodies out of task URLs and bound the slug A task's instruction is a prompt body, not a title, so it must not reach a path segment — from there it lands in browser history, the analytics page view fired on every history change, access logs, and anything pasted from the clipboard. That was already true on canary for the Recent feed, which links a task by the display title (`COALESCE(name, instruction)`); the canonicaliser happened to strip it on arrival. Rather than making that fallback stick everywhere, drop it from URLs entirely: `slugTitle` carries `tasks.name` alone through the Recent payload, and the canonicaliser, the copied link and the Portal header all read `name`. A nameless task simply has no slug, and every surface now agrees on that instead of rewriting each other. Grapheme counting is also not a size bound. A single cluster accepts an unlimited run of combining marks, so `a` plus 10k accents stayed one grapheme and produced a ~20KB segment, past what browsers and proxies accept in a request line — and titles arrive from imported and generated content, not only from typing. Keep whole clusters, but stop at a code-point ceiling as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🌐 chore: update i18n Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🐛 fix(recents): move a task's slug source with an inline rename Splitting the link's slug source out of the display title left the two able to drift: renaming a task from Home updates `title` optimistically and on commit, but `slugTitle` kept the server's old name, so the row went on linking to the previous title's slug until the next sync. That is the same link-vs-canonicaliser disagreement this feature set out to remove, reintroduced one field over. A rename sets `tasks.name`, which is exactly what `slugTitle` carries, so both the committed reducer path and the optimistic selector overlay now move it with the title. Topics and documents have no slug source and keep theirs null. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
35098de372 |
✨ feat: add readable task URL slugs and workspace seat copy (#19503)
* ✨ feat: add readable task URL slugs and workspace seat copy Task detail URLs gain a cosmetic `/task/:taskId/:slug` segment. The slug is built from the task title and never participates in resolution — `taskId` stays the only key — so stale or hand-edited slugs still resolve, and `useCanonicalTaskSlug` rewrites the URL to the current title. Also bundles the workspace seat locale strings for pending-invite seat accounting, and the knowledge base library visibility confirmation copy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(document): scope the library mirror update to the caller's own file `setVisibility` mirrors a page's visibility onto its paired `files` row, but scoped the update by file id and workspace alone. `fileId` is a plain foreign key and a document can point at a file somebody else uploaded — parsing another member's shared file yields a caller-owned document that keeps the original `fileId` — so taking such a derived page private also hid the uploader's original from the whole workspace. Require `files.userId` to match the caller, the same guard the `documents` update in this transaction already applies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(task): collapse the task URL when the title is cleared `useCanonicalTaskSlug` gated on the title itself, so a resolved task whose name is empty looked identical to one whose detail had not loaded yet. Clearing a task title persists `name: ''` (the input saves the raw value), which left the URL pinned to the slug of the title that had just been deleted. Read loaded-ness separately from the title so an empty name is honoured and the URL collapses back to `/task/:taskId`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(task): stop the My tasks board from hiding automated tasks The "My tasks" board copied `automated: false` from the boards around it, but its list view sends no automation filter at all. A scheduled or heartbeat task assigned to the caller therefore disappeared the moment they switched that same collection from list to board — the one place the two views must agree. Extracts the scope-to-query mapping into `buildKanbanGroupQuery` so the rule is stated once and covered by unit tests instead of living in a nested ternary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
6b85e332f1 |
🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile (#19462)
* 🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile The main hash accepted any npm module that happened to resolve inside apps/desktop/node_modules, so a module linked from outside the frozen install (or from a stale store entry) could enter the renderer OTA hash without the lockfile ever pinning it. Resolve each bundled module to its package name@version and require apps/desktop/pnpm-lock.yaml to pin it, keeping the file:/link:/git/tarball escape hatch for specifiers that pin by resolution instead of version. partial-json is imported by packages/utils and packages/context-engine but was never declared, so the Desktop lockfile could not pin it: declare it in both packages and record it in apps/desktop/pnpm-lock.yaml. * 🔊 chore(desktop): name the changed bundles before a dev restart The dev orchestrator logged only that a main/preload bundle changed, which cannot distinguish the debounced restart from an unrelated vite rebuild. Collect every file reported by the watcher during the debounce window and print it with its event type, size and mtime. * 📝 docs(acceptance): publish the round from the session's own observations A delivery verified on the real product earlier in the session still had to re-run the plan and checker stage to obtain a published round. Document that those observations and artifacts are the evidence: write plan[] and cases[] from them, disclose anything never captured, and ingest. |
||
|
|
aba9e8150c |
👷 build: migrate the test suite to vitest 5 (#19355)
* 👷 build: upgrade vitest to 5.0.0 Bump vitest and @vitest/coverage-v8 to 5.0.0 and migrate the configs for Vitest 4/5: drop the removed coverage.all option and replace poolOptions.forks.singleFork with maxWorkers/isolate. * 👷 build: shim jest-dom matcher types for vitest 5 Vitest 5 exposes Assertion<R, T> and no longer reads the jest.Matchers augmentation that @testing-library/jest-dom ships, so its matchers vanish from expect(). Re-declare them on the Vitest interface, and replace the removed describe.sequential with { concurrent: false }. * 👷 build: migrate the test suite to vitest 5 Vitest 5 (and @vitest/coverage-v8) replaces the esbuild dep optimizer with Rolldown, removed environmentMatchGlobs/coverage.all/poolOptions, forwards `new` to mock implementations, requires top-level vi.mock, and no longer resets automocks in restoreAllMocks. Migrate the root config to app/server projects with Rolldown optimizer options, shim the jest-dom matchers on Assertion<R, T>, and update the affected tests (constructor mocks, hoisted mocks, awaited assertions, explicit mock types, per-test mock defaults). * 👷 build: make the openapi knowledge-base FileService mock constructable tests/ was outside the per-area sweep; vitest 5 forwards `new` to the mock implementation, so the arrow implementation threw "is not a constructor". * 👷 build: point the CI blob-report artifacts at the vitest 5 output dir Vitest 5 writes blob reports to .vitest/blob instead of .vitest-reports, so the sharded jobs uploaded nothing and the merge jobs failed with ENOENT. Track the new directory in .gitignore too. * 👷 build: refresh the desktop lockfile for vitest 5 apps/desktop/pnpm-lock.yaml is committed and the desktop build action installs with --frozen-lockfile, so the vitest/override change has to be reflected in it. * 👷 build: keep the pre-vitest-4 coverage exclusions explicit Vitest 4+ ships an empty coverageConfigDefaults.exclude, so configs that spread it silently lost the default exclusions (node_modules, dist, *.config.*) and counted more files. Inline the previous list. * 🎨 style: format the migrated vitest configs * 👷 build: keep canary's new class mocks constructable under vitest 5 exploreGraph/dispatchAuthorization/scopedDevices (canary #19375, #19352) mock classes with arrow implementations; vitest 5 constructs the implementation when the mock is newed, so the arrows throw 'is not a constructor'. |
||
|
|
f4a39da7d9 |
✨ feat(goal): let exploration revise a flawed protocol instead of only branching (#19375)
* ✨ feat(goal): let exploration revise a flawed protocol instead of only branching The exploration planner could only `expand` or `verify`, so when an experiment measured the wrong thing its only move was a sibling that inherited the same flawed instrument. It also had no way to tell which experiments a previous turn authored, so it could not see that its last branch had made things worse. - Add a `revise` action that re-runs one experiment with a corrected protocol inside the parent's container, without consuming an experiment slot. - Bound revisions per target via `derived_from` edges so a planner cannot keep polishing one instrument instead of changing the question. - Pass `derivedFromId` to the planner so it can compare a derived experiment against its parent before repeating that direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): separate a protocol correction from an ordinary branch Codex review found three ways the revision path and the expansion path were conflated, because both write a `derived_from` edge. - The revision cap counted every `derived_from` edge at the target, so an experiment with two ordinary branches was refused its first correction. Count only edges a task sourced, which is what a correction is. - A standalone correction read back as an experiment: it spent an experiment slot and could then be revised as a fresh target with an empty budget, an unbounded chain. Exclude corrections from the experiment selector on both the planner and the apply side so the two see the same graph. - A dispatched correction resolved its provenance through its container, which carries the container's own branch parent, so the run received the results of an older experiment. Prefer the node's own provenance edge. The lookup moves to `provenanceParentId` in `@lobechat/utils/goalGraph` next to the two new predicates, so the rule is unit-testable rather than inline in a 600-line dispatch path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): keep a correction's evidence and stop a spent allowance pausing the goal Two consequences of the previous commit, both found in review. - Excluding corrections from the experiment selector also hid their results. `experimentResults` only walked containment, so a standalone correction's findings were folded into neither itself nor the experiment it corrected, and the next exploration turn evaluated pre-correction evidence only. Gather an `experimentScope` that includes the corrections aimed at a node, and use it for produced Work versions too. - A spent revision allowance threw, and `exploreGraph` treats a throw as a planner crash: it paused the whole goal, and resuming could reach the same choice again because nothing told the planner the allowance was gone. Return it as an outcome that re-plans, and put `revisionsRemaining` in the planner input so the choice is observable before it is made. `MAX_PROTOCOL_REVISIONS` moves next to the revision predicates in `@lobechat/utils/goalGraph`, since both the model and the planner input need it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): give corrections a container, a released lease and a documented contract Four more from review, all consequences of the correction path being bolted onto a graph shaped for branches. - A spent allowance returned without clearing the checkpoint, so the re-plan it asked for was refused by the next claim until the lease timed out. Release it like every other terminal path. - A correction of an uncontained seed was another root task: invisible to the planner, unable to inherit the previous correction's Work, and read back as a slot-consuming experiment by an older release. Require an experiment container and let those goals expand first, which removes all three at once. Both apply paths now gather `experimentScope`, so a correction's Work carries forward. - `revisionsRemaining` reports zero for a node that cannot hold a correction, rather than inviting a choice the apply step would refuse. - `EXPLORATION.md` documented two actions. It now covers the third, the slot-free guarantee, the per-experiment bound and why corrections are serial. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): record corrections on their own edge kind Review found `derived_from` was the wrong carrier. It is generic provenance that predates this feature, `goal.addEdge` accepts it, `createEdge` puts no task-source restriction on it, and an existing dispatch test authors exactly that shape. Treating any task-sourced one as a correction therefore reinterpreted hand-authored provenance: those tasks vanished from planner history and slot counting while their results were attributed to another node. Corrections now use a dedicated `revises` kind, written only by the coordinator and deliberately absent from the public `addEdge` vocabulary, so the discriminator is unambiguous and nothing existing changes meaning. The column is plain text, so no migration is involved. Also stop replanning on a rejected revision. Returning `advanced` re-invoked the planner with the same graph and the same `revisionsRemaining: 0`, which could spend an advance's whole tick budget repeating one question; the advance now ends and the goal stays running for the next wakeup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(goal): note the dedicated revises edge in the exploration contract Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): park a goal whose planner asks past a spent revision allowance Review found ending only the current advance left the goal running with an unchanged graph, so every sweep could buy another identical planning call indefinitely, and enough such goals would crowd the sweep's bounded scan. The previous round's `advanced` had the same defect inside one advance. Park it instead, the way an exhausted experiment budget already does: `pausedBy: 'exploration_revision_limit'`, a recorded status transition naming the target and the count, and `no_progress` carrying that reason. Resuming replans. The new reason is deliberately not treated as budget-stopped, so raising a budget does not silently reopen a goal that needs a different plan. Also documents the rollback shape for a queued correction: the previous coordinator resolves provenance through the containing experiment, so a rolled back deployment dispatches it with that experiment's branch parent instead of the one it corrects. The corrected protocol lives in the Task description and survives, so the loss is context rather than a wrong instruction; the note says to drain queued corrections first if that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c04405343a |
✨ feat(goal): add nested experiment answers and exploration navigation (#19261)
* ✨ feat(goal): add nested experiment answers and exploration navigation * 📝 docs(goal): correct exploration budget command * 🐛 fix(cli): isolate dispatched agent run identity * 🐛 fix: register CLI notebook reports as operation deliverables * 🐛 fix: avoid signing notebook origins as file URLs * 🐛 fix(cli): retain bounded event backlog across temporary outages |
||
|
|
5512942ba4 |
🐛 fix: clamp message plugin identifier/apiName to 255 chars at every write path (#19002)
* 🐛 fix: clamp message plugin identifier/apiName to 255 chars at every write path Hallucinated tool calls have written multi-KB values (artifact XML, shell commands) into message_plugins.identifier / api_name. With those columns now btree-indexed, any value past the index tuple size limit fails the whole row insert, so every write path clamps before hitting the database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ✅ test: cover message plugin identifier clamping * ✅ test: include plugin arguments in clamp fixture --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5590527309 |
✨ feat(file): support .v / .sv (Verilog / SystemVerilog) attachments (#19118)
* ✨ feat(file): support .v / .sv (Verilog / SystemVerilog) attachments Wire HDL source files through the full attachment chain so .v/.sv files upload, parse, and reach the model like other text code attachments: - uploadGuard: add `v` / `sv` to the chat document-extension whitelist so plain-chat uploads are no longer rejected - utils/mimeType: map `.v` → `text/x-verilog`, `.sv` → `text/x-systemverilog` in CUSTOM_MIME_TYPES (mime-db has no entries for either, so they used to resolve to `application/octet-stream`) - file-loaders: add `v` / `sv` to TEXT_READABLE_FILE_TYPES so the server DocumentService.parseFile dispatch uses TextLoader instead of throwing UnsupportedFileTypeError — this is what empties the file content injected into the model context via filesPrompts - FileViewer: route `.v` / `.sv` into the Code renderer (extension entries plus bare tokens and text/x-* mimes in CODE_MIME_TYPES) instead of the NotSupport view - fileLanguage: map `.v` → `verilog`, `.sv` → `system-verilog`. The shiki bundled grammar id is `system-verilog`; `systemverilog` is not a bundled id/alias and would silently degrade highlighting to plaintext Tests: upload admission (empty/octet-stream/uppercase mime), HDL mime resolution with charset, loadFile content extraction from real .v/.sv fixtures, filesPrompts context injection, shiki grammar id validity, token-accounting text-like classification, and lookalike-extension negatives (.vsv/.vv). Existing behavior for other types unchanged (.zip still rejected, .pdf still uses the dedicated loader). * refactor(file): route FileViewer HDL entries through the shared verilogSupport module Address codex review on PR #19118: - FileViewer CODE_EXTENSIONS / CODE_MIME_TYPES now spread the canonical VERILOG_FILE_EXTENSIONS / VERILOG_FILE_MIME_TYPES from ./verilogSupport instead of keeping private copies, so the regression test protects the shipped routing — dropping an entry from either side fails the suite instead of silently regressing .v/.sv previews to NotSupport. - verilogSupport: the guard now matches the same text/x-* mime tokens the production router consumes, and the canonical entries are exported as the single source of truth. - uploadGuard.test: drop the duplicated upload-admission test block (identical purpose and assertions as the preceding test). * fix(file): apply chat-side subtree patch missed in previous commit * fix(file): correct corrupted verilogSupport blob (sv entry, mime list literal) * 💄 style(file): fix import order in FileViewer (simple-import-sort) CI lint failed on simple-import-sort/imports — the verilogSupport import inserted via the review fix sat above the './NotSupport' block. eslint autofix moves it to its sorted position after './Renderer/Video'. |
||
|
|
f23693b121 |
♻️ refactor(acceptance): converge agent-testing into the acceptance skill (#18830)
* 🔧 refactor(cli): commit the installed acceptance skill instead of ignoring it `lh acceptance install` used to write the materialized skill into a nested `.agents/skills/.gitignore`, keeping it out of the consuming repo's history. That made every skill change invisible to review and impossible to pin to a commit. The skill is a reviewable team asset like any other file, so installing it no longer touches any ignore file: `ensureSkillIgnored` and the `--no-gitignore` flag are gone, and wiring only creates the harness symlink. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(acceptance): make GOMS-KLM interaction cost a platform-owned measurement Interaction cost used to be computed by a skill-local analyzer script: the timing model lived in the driver, so a published number could not be checked against its trace and two rounds were not necessarily priced the same way. Split the seam. A UI driver now records only raw operator counts into `interaction-trace.jsonl`; `summarizeKlmTrace` in `@lobechat/utils` is the one place counts become seconds, against the timing model pinned in `@lobechat/const/verify`. `lh acceptance run ingest` picks the trace up from the report directory and prices it itself, so no analyze step can be forgotten and every round is comparable. Interaction cost stays an optional overlay. No trace — a CLI round, or a machine without `agent-browser` — publishes exactly as before, silently. A trace whose actions were all blocked (the shape a missing driver produces) yields no cost rather than a 0s journey, and an explicit `result.json.interactionCost` is left untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(acceptance): converge agent-testing into the acceptance skill + project layer Two skills described the same job. `agent-testing` (repo-local, 5.4k lines) and the portable `acceptance` skill each carried their own copy of the report schema, the agent-browser reference, and the CLI/web/Electron surface guides — already drifting apart while both fed the same `lh acceptance run ingest`. Collapse them into one skill plus one project layer: - `acceptance` owns the contract — what a check is, what evidence counts, what a valid round is — and now also the generic living logs and the project-adapter contract. Its entry point reads `.agents/acceptance/` first when the repository has one, and its triggers absorbed agent-testing's ('local test', 'test in electron', …) so no invocation loses a route. - `.agents/acceptance/` owns how THIS repository runs: the new `PROCESS.md` (target grounding, living-log admission check, environment/auth gates, approval gate, surface choice, publishing, teardown), plus the capture scripts and macOS how-tos that were only ever generic by accident. - `.agents/skills/acceptance` is a symlink onto the skill source, so in this repo the skill cannot drift from what ships. `agent-testing` is deleted. Its analyzer script goes with it (ingest computes the cost now); its KLM smoke test keeps asserting operator counts, and the pricing half of that assertion moved to the shared module's unit test. Consumers — agent-testing-bot, debug-frontend-with-browser, ux-audit, skills-audit, heterogeneous-agent, ux — are repointed. Also drops `skillLocator`, dead since nothing called it and the bundled skill directory it resolved never existed in this repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(acceptance): version the acceptance skill in its own frontmatter An installed skill was indistinguishable from any other copy of it: nothing on disk said which revision a builder was holding, so `install` could only blindly skip an existing file or blindly overwrite it. Declare `version` in SKILL.md's frontmatter and parse it from there. The frontmatter is the single source of truth precisely because that same file is what gets materialized onto disk — so an installed copy always states the version it carries, and a later install can compare the two without a side-car marker that drifts from the content it describes. The version flows out through `getSkillBundle` and into the install result, which is what a future version-comparing update needs; this commit stops at making the comparison possible. `version` is optional on BuiltinSkill: every other builtin skill stays unversioned, and an unversioned bundle installs exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔧 fix(acceptance): stop coupling the skill to host environment variables The skill told builders to branch on `$LOBE_OPERATION_ID` and `$LOBEHUB_TOPIC_ID`. Both were wrong: - `LOBE_OPERATION_ID` exists nowhere in this codebase. An agent following the document literally always found it empty, so the entire "two entry points" framing rested on a variable that was never set by anything. - `LOBEHUB_TOPIC_ID` is real but is read by the CLI itself (`subjectFromEnv` / `originFromEnv`) to resolve subject and origin. Naming it in the skill invites an agent to go hunting for an id it never needs to touch, and couples a deliberately portable skill to one host. The skill now states the actual contract: it needs no ids at all. You have an operation id only when the invocation hands you one; `--subject` is for when the caller named a Task/Topic/Document; otherwise ingest attaches the round or creates a standalone acceptance on its own. Examples use a plain `$OPERATION_ID` shell variable, which carries no claim about who exports it. A test now asserts the bundle names no `LOBEHUB_*` variable, so the coupling cannot quietly return. Version bumped to 0.1.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): price a trace even when result.json scaffolds a null cost Found by running the converged chain end to end, which is the only way it could have been found: `report-init.sh` writes `"interactionCost": null` to document the field, and the ingest guard treated mere key presence as "the author already computed this". So the project's own report scaffolder silently suppressed pricing on every traced round it created — and no unit test caught it, because a test fixture is hand-written, not scaffolded. Only an object now counts as an author-supplied summary. A null is the absence of a measurement, not an assertion that the round cost nothing. The same run also showed the shipped `interaction-cost.md` overstating where the cost appears: it claimed the acceptance page renders it "next to its checks", but it is a collapsible section of the round report, reachable through owner-only round history (`canViewAcceptanceHistory = isOwner`) and absent from the shared aggregate. Corrected to describe the real surface, with an explicit warning not to promise a reviewer they will see it. Amends generic mistake M1 with its mirror case: proving a UI section is ABSENT by grepping page text is worse than the positive case, because an acceptance whose own goal sentence names the feature matches its own prose. Assert presence or absence against structured state instead — this run hit exactly that false positive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(acceptance): show interaction cost to the reviewer, not just the owner Two fixes for the same root problem — the KLM cost existed but nobody who needed it could see it. **The scaffolder wrote the null.** `report-init.sh` emitted `"interactionCost": null` to document the field, which is what made the previous commit's ingest guard misfire. Remove it at the source: a scaffolded report now simply omits the key, and ingest prices its trace. The guard stays as defence in depth for hand-written reports, but the scaffolder no longer arms it. **The panel was in the wrong place.** `InteractionCostPanel` lived only in the round report, which opens from owner-scoped round history (`canViewAcceptanceHistory = isOwner`). So the one audience the number exists for — a reviewer weighing whether a flow is worth accepting — could never see it, while the owner who already knows the cost could. Surface it on the acceptance page itself, collapsed by default, showing total/active/wait inline and the full per-phase breakdown on expand. No server or permission change is needed: the cost already ships in the shared bundle (only `origin` is stripped for visitors). Phases attributed to a check (`--klm-check` → `phase.checkItemId`) now render that check's title instead of an internal slug, so the cost reads against the thing being judged. Splits `ReportViewer` (1956 → 1537 lines) by extracting the panel, and its pure pricing/reading helpers into `interactionCostModel.ts` so round selection is unit-testable without mounting React — the repo does not take new component tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): keep the phase name readable next to its check title Caught by finally looking at the rendered panel on a seeded local stack: the check title was `flex: 0 0 auto`, so in a narrow column it squeezed the phase name out entirely and then clipped itself mid-word. Both phase rows ended up reading as the same truncated check title, which is worse than no attribution — the phase is the only thing telling two rows apart. The phase name now holds its width (capped at 60% of the column) and the check title is the part that gives way and truncates, with the full pair still in the row's tooltip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 refactor(acceptance): make interaction cost a quiet disclosure, not a card The first pass gave it a bordered block with a title, a round pill and three inline metrics, sitting directly above the check list — the same visual weight as the acceptance goal card. Supporting detail was competing with the checks for the attention a reviewer should be spending on the decision itself. Reduced to the disclosure pattern already used for "逐项验收": a small text button carrying only the one number worth seeing at a glance, with the full breakdown one click away. The round moves into the expanded panel's header beside the model id, where it is read rather than scanned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): move interaction cost below the check list Review feedback on the round-5 evidence, circled on the collapsed row: it does not belong inside the acceptance check list, it belongs after it. Right — the list is the decision surface, and a row sitting at its head reads as one more item to review even when it is styled quietly. Moved below the list, between the checks and the decision bar, where supporting detail is read after the thing it supports rather than before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Arvin Xu <arvin.x@lobehub.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
860bc7d60b | 🐛 fix(artwork): normalize reference image sources (#18742) | ||
|
|
8e100aaa3b | 🐛 fix: clarify Ox redirect and preserve model prices (#18727) | ||
|
|
bc08dbe17d |
♻️ refactor: make API key prefix and CLI product naming distribution slots (#18637)
* ♻️ refactor(cli): collect every product-naming string into one module
The CLI's identity was spread across a dozen files: bin names in
`utils/completion.ts`, the config directory duplicated in five places, the
systemd unit name defined twice, the product name in a handful of command
descriptions, and — least visibly — inside `buildNotifyProtocol`, a prompt
handed to heterogeneous agents that the model then repeats back to the
user.
That is fine for a single distribution and leaves a white-label build with
no seam. Several of these are reached at runtime along paths no UI gating
can intercept, and one of them is a prompt, so there is nothing to gate.
`src/constants/identity.ts` now holds all of them. Values are upstream's,
unchanged, and the default build's observable output is identical —
verified by building with this repo's own config and diffing what the CLI
prints:
Usage: lh [options] [command]
LobeHub CLI - manage and connect to LobeHub services
complete -o nosort -F _lobehub_completion lh lobe lobehub
lh - LobeHub CLI - manage and connect to LobeHub services
Two things that were already wrong, fixed on the way:
- `skillLocator.ts` compared `pkg.name` against a hardcoded
`'@lobehub/cli'`, so the package-root walk breaks for any fork or
rename. It reads `cliPackageName` now, which is the same value from
the same package.json.
- `constants/auth.ts` exposed a single API-key env var name. It is a
list now, most-preferred first, so a distribution can introduce its
own prefix without breaking a variable a user already exported. Only
USER-set variables moved; `LOBEHUB_SERVER`, `LOBEHUB_JWT` and the
agent-run ids are injected by the desktop app and the server, so
renaming those needs all three sides to move together and is left
alone.
`man/roff.ts` also stops hardcoding `LH` and `@lobehub/cli` in the roff
header, taking them from the bin name and `pkg.ts` instead — the man page
described a command that would not exist under any other name.
* ♻️ refactor(api-key): make the key prefix a slot instead of a literal
`sk-lh-` was hardcoded in four places and asserted in four test files. It
is the one brand string a deployment's end users actually handle — they
copy it out of the UI, paste it into CI config, quote it in support
threads — so on a white-label distribution it undoes the rebranding
everywhere else.
It is also load-bearing rather than cosmetic. `validateApiKeyFormat` is
what the OpenAPI auth middleware uses to tell an API key from an OIDC
JWT, so generation and validation must read the same value or every
issued key fails auth as a malformed JWT. That is why the prefix is one
constant now, used by both.
`API_KEY_PREFIX` defaults to `sk-lh-`, so upstream's behaviour is
unchanged — verified by generating a key and round-tripping it through
the validator.
Two deliberate choices in the implementation:
- Validation compares the prefix with `startsWith` and tests only the
16-character suffix against a static pattern, rather than
interpolating the prefix into a RegExp. The prefix is data flowing
into a pattern, and escaping it correctly is a thing to get wrong;
this way there is nothing to escape and the check stays correct for
whatever prefix a distribution picks.
- The tests derive their fixtures from the constant instead of
hardcoding a spelling, so they pass for any prefix. Several of them
fed a literal straight into `validateApiKeyFormat`, so they would
have gone red the moment anyone overrode it — failing on the
override rather than on a real defect.
`spec.ts`'s `bearerFormat` follows the constant too; it is the public
API document, so it was advertising a format the deployment no longer
issues.
* ♻️ refactor(cli): collect the completion word-index env var into identity.ts
CLI_COMPLETION_CWORD_ENV joins the other product-naming strings in
identity.ts. `LOBEHUB_COMP_CWORD` was the one string the earlier
identity refactor (
|
||
|
|
2a3b27a76d | 🐛 fix(mcp): tolerate prototype keys in desktop MCP IPC and parse pasted stdio commands (#18617) | ||
|
|
5072403ae0 |
🐛 fix(agent-runtime): stop sanitizeNullBytes from corrupting escape text (#18507)
* 🐛 fix(agent-runtime): stop sanitizeNullBytes from corrupting escape text sanitizeNullBytes round-tripped objects through JSON.stringify -> string surgery -> JSON.parse. On any payload whose TEXT spells out a unicode escape for U+0000 -- regex character classes, GROUP_KEY_SEP constants, smali / Kotlin metadata, base64 dumps, i.e. everything a coding agent reads out of real source files -- stringify doubles that text's leading backslash, so the six-character needle matches one character late. Stripping it eats the escape's own backslash and leaves an orphan trailing backslash, and the re-parse dies with `SyntaxError: Bad escaped character in JSON at position N`. Thrown from the tool-result persist path (messagePlugins.state), that killed the whole operation: 316 failed agent_operations across 116 users since 2026-04-30, provider-agnostic, burning entire multi-minute runs. Walk the value and rewrite decoded strings instead, the way stripNulDeep in @lobechat/heterogeneous-agents already does. Decoded values can never reach an escape sequence, so literal escape text survives untouched. The corrupted-Unicode recovery (NUL + two hex digits -> the character it meant) is preserved, just applied to string values rather than JSON text. Also persist a bounded `stack` on formatErrorForState's unclassified Error branch. `name` + `message` alone made this class of 500 impossible to locate from a stored operation -- the pg branch already keeps its extra context, this one dropped everything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(model-runtime): classify harness JSON.parse throws as E7008 A `SyntaxError` out of a harness-side `JSON.parse` landed on the bare `InternalServerError` fallback: no attribution, no category, no severity, no `countAsFailure`, no stable id. 316 such operations over four months were indistinguishable from any other 500 on the dashboards. Add `HarnessJsonParseError` (E7008, category `stream`, attribution `harness`, `countAsFailure: true`, `retryable: false`) and register the two V8 phrasings — "… in JSON at position N" and "Unexpected end of JSON input" — in the pattern registry. Placed after every provider section so an upstream body quoting a parse failure keeps its provider code, and before the `AgentRuntimeError` crash fallbacks so this class does not dissolve into that catch-all bucket. `retryable: false` states the fact rather than changing behavior: the same corrupt payload re-parses to the same failure, so retrying only re-burns the run's tokens. Nothing in the agent runtime acts on `retryable` today — it is classification metadata. Ships the en-US source string plus the hand-written zh-CN mirror; the remaining locales fall back to English until the daily i18n workflow picks them up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eff0740fb4 |
✨ feat(portal): publish workspace HTML as a public Artifact (#18424)
* ✨ feat(portal): publish workspace HTML as a public Artifact Add a Publish action on Local File HTML preview. Collect local static assets from HTML, CSS, and JS (including Vite import.meta.url hops), inline files under 32 KB, and keep larger or JS-resolved files as a small static site. * 🐛 fix(portal): keep workspace HTML publish identifiers and assets intact Rewrite unquoted local hrefs, hash the original path into the artifact id, and read sandbox binaries as bytes instead of text. * 🐛 fix(portal): pack Files-tree HTML publish and keep local assets Treat macOS /private/tmp as the same workspace as /tmp, refuse leftover relative hrefs, and emit SVG data URIs with image/svg+xml. Also add a Files-tree publish action and collapse packing details in the confirm. * 🐛 fix(portal): rewrite every href spelling and scope publish state Keep each spelling of a deduplicated asset and rewrite HTML refs through the base-resolved path collection already produced, treat root-relative refs as remote under an absolute <base>, and enforce the file-count and byte limits while reading instead of after. Look up deployments with the same canonical workspace-relative identifier gathering publishes, and drop a publish result that finishes after switching file or topic. The OSS business slot now reports the publisher unavailable; the cloud override carries the market-deployment implementation. * ♻️ refactor(portal): dedupe workspace HTML publish pipeline and pack once Carry the prepared pack result through the publish input so the site is packed once per publish, and share one executor plus error mapping across the preview button and Files-tree entry points. Extract the HTML tag scanner shared with HtmlPreview, reuse getMimeType / base64 / escape helpers from shared packages (fixing sidecar content types for extensions the local table missed), route desktop byte reads through projectFileService, batch leaf asset reads, and fold the slot predicates and duplicated identifier derivation into single homes. |
||
|
|
3fc02f5501 | ✨ feat(desktop): complete Codex app-server session lifecycle (#18275) | ||
|
|
a58d181309 |
✨ feat(chat): support web voice messages (#17744)
* ✨ feat(chat): support web voice messages * 💄 style(chat): align voice message composer * 💄 style(chat): match voice recorder input scale * 🐛 fix(chat): animate voice message waveform * 💄 style(chat): preserve composer height during recording * 🐛 fix(chat): ignore inherited voice send hover * ✨ feat(chat): normalize voice messages across providers * 🐛 fix(chat): make voice message retries cancellation-safe * 🐛 fix(chat): restore LobeHub voice capability * 🐛 fix(chat): make voice messages optimistic and context-safe * 🐛 fix(chat): drop obsolete topic loading cleanup * 🐛 fix(chat): align voice capability gating * ✅ test(chat): fix voice context typing * 🔨 chore: resolve canary rebase conflicts * ✅ test(database): align audio metadata query expectations * 🐛 fix: block voice recording while generating |
||
|
|
cd474dfcc3 |
✨ feat(model-runtime): estimate audio input costs (#17949)
* ✨ feat(model-runtime): estimate audio input costs * 📝 docs(model-runtime): clarify audio usage comments * 🐛 fix(model-runtime): avoid ambiguous cached audio billing * 🐛 fix(file): persist uploaded audio duration |
||
|
|
86991beb75 |
♻️ refactor: inject graph runtime context continuously (#17761)
* ♻️ refactor: inject graph runtime context continuously * ♻️ refactor: clarify graph node tool guidance * ♻️ refactor: generalize runtime additional context * ♻️ refactor: simplify additional context injection * ♻️ refactor: tighten runtime context provider * ♻️ refactor: group graph agent files * ♻️ refactor: separate graph context hooks * ♻️ refactor: compact graph prompt hook application * ♻️ refactor: share JSON-safe conversion utility * ♻️ refactor: keep runtime context at virtual tail * ✨ refactor runtime additional context placement * ♻️ remove unused virtual tail override * 📝 rename runtime additional context types * 🧹 clean up runtime executor context fixture * ♻️ tighten runtime additional context contract * 🧹 clean up runtime executor test diff * 🧹 drop unnecessary context phase comment diff |
||
|
|
0334243690 |
♻️ refactor(topic): remove legacy paused status and hash icon (#18036)
* ♻️ refactor(topic): remove legacy paused status and hash icon * 📝 docs(topic): remove paused status references |
||
|
|
4230dcdddc |
✨ feat(chat): let the client mint topic and message ids (#17889)
* ✨ feat(chat): let the client mint topic and message ids Ids are plain `nanoid/non-secure` values behind a namespace prefix — no sequence, no server secret, no database state — so the client can name a row before it exists and the server can honour it verbatim. Doing so removes the placeholder-id → real-id swap that the whole send flow is currently built around. Server (accepts, does not require): - `newTopic.id`, `newUserMessage.id` and `newAssistantMessage.id` on `AiSendMessageServerSchema`, each validated against the namespace's id shape. An unvalidated client primary key would let a caller submit look-alike ids, wrong namespaces, oversized values, or strings that leak into logs and URLs. - `MessageModel.createUserAndAssistantMessages` takes an `ids` option; `TopicModel.create` already accepted one. Both still mint when none is given, so an older client keeps working and the two sides deploy independently. - A primary-key collision now surfaces as CONFLICT rather than a 500. The realistic trigger is a retried send replaying its ids, which is client-correctable; the message is generic so a caller cannot probe for rows it may not read. Client: - `sendMessage` mints the user, assistant and topic ids up front and sends them, so the optimistic rows are created under their final ids. - The post-response temp-message cleanup is gone: the rows the server persisted are the rows already on screen, so deleting them would delete the real ones. Rollback on cancel/failure is kept. `generateEntityId` mirrors `idGenerator` (same alphabet, same prefixes); the prefix table and validator live in `@lobechat/types` so the edge schema and the client generator share one definition without a dependency cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(topic): track optimistic topic ids instead of sniffing their prefix `#reconcileFetchedTopics` preserved in-flight first-send rows by testing `id.startsWith('tmp_topic_')`. Client-minted ids made that dead code: the placeholder now carries a real `tpc_…` id, so the guard silently stopped matching and a topic-list refetch landing mid-send wiped the row. The consequences went past a missing sidebar row. `topicLoadingIds` is keyed by that id, so the send's spinner vanished with it; and `replaceTopicId` bails on `topicIndex === -1`, so once the row was gone `resolveOptimisticTopic` had nothing to reconcile onto — the metadata carried on the placeholder (`workingDirectory` / `repos` for heterogeneous runs, the pinned model snapshot, the title) was dropped. Gateway mode is the worst case: `execAgentTask` takes no topic id, so the server still mints its own there. The client id and the server id never converge, which means the placeholder is never in a fetched list and was unprotected for the whole creation window. Track the ids explicitly in the topic slice instead. Bookkeeping lives in `internal_dispatchTopic` — the single funnel every add / replace / delete goes through — so a caller cannot register a placeholder and forget to clear it, and so the guard cannot silently rot the next time the id format changes. Regression tests pin all three transitions (preserved across a refetch, released once the server id is known, released on rollback); the first one fails against the prefix test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(gateway): forward client-minted ids through execAgent The gateway send path (`executeGatewayAgent` → `aiAgent.execAgent`) was the one send route left minting its own ids: `ExecAgentTaskParams` had no id fields, so the server created the topic and the user/assistant pair under fresh ids and the client's optimistic rows never converged with the server rows. That kept the whole placeholder-reconciliation machinery (`internal_replaceTopicId`, temp-row refetch) load-bearing for gateway mode, and left the optimistic topic row protected only by the explicit-id tracking fix for its entire creation window. Bring it to parity with `sendMessageInServer`: - `ExecAgentParams` / `ExecAgentSchema` gain an optional `clientIds` block (`topicId` / `userMessageId` / `assistantMessageId`), validated per namespace at the edge — the same gate as the aiChat schema, for the same reason: an unvalidated client primary key is an attack surface. - `execAgent` forwards them to `topicModel.create` and the two `messageModel.create` calls. Absent → the models mint, so older clients and every non-chat caller (cron, bot, task) are untouched. - Resume-like replays (`resume`, `resumeApproval`, `resumeToolResult`, `parentMessageId`) drop `clientIds` defensively in the service: a replayed id would collide with the rows the original send already created, and trusting every caller to omit them is exactly the kind of contract that rots. - A 23505 on those ids surfaces as CONFLICT (retried send replaying its ids — client-correctable), not a 500; generic message so ids can't be probed. - `sendMessage`'s gateway branch now passes the same three ids its optimistic rows already render under. Contract tests pin all three behaviours (forwarded on fresh send, absent → server mints, dropped on resume-like replay). Existing single-argument `toHaveBeenCalledWith` assertions on the create spies gained the new id argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(chat): adopt the minted topic id for the whole first send The final piece of the client-minted-id migration: the conversation no longer starts at `topicId: null` and switches after the server responds. `sendMessage` mints the topic id up front and the ENTIRE send adopts it — the operation context, the optimistic message bucket, `activeTopicId` and the sidebar row all carry the final id from the first painted frame. That removes the `_new` → topic transition, which was the root of the first-send flicker: the context key changed, `ConversationProvider` re-keyed, the subtree remounted, and virtua repainted from an empty viewport (~50ms of zero rows). With the key stable end to end there is nothing left to remount. Measured in a real browser, the first send's DOM sequence is now `0 → 2 (optimistic ids) → …` with no empty frame and no id swap, in both the client and gateway runtimes. How the pieces fit: - `willCreateNewTopic` replaces every `!operationContext.topicId` test — the context now carries an id for a topic that has no server row yet, so "is this send creating the topic" must be an explicit flag. - The server keeps seeing the OLD shape: no `topicId` while creating, the minted id rides in `newTopic.id` / `clientIds.topicId`. For gateway this splits the call's contexts: execution context (server: create) vs message context (client: the bucket already on screen). - `creatingTopicIds` (promoted from the action-private set) names the window where the topic exists only client-side. Message fetch/prefetch/revalidate skip those ids — the query would legitimately return an empty list and wipe the optimistic messages. Cleared on `replaceTopicId` / `deleteTopic`, so fetching resumes the moment the server confirms. - The queue check probes the pre-mint `_new` key and the creating buckets in addition to the send's own key: a fast second Enter must queue behind the send still creating its topic, whose op is registered under ITS minted bucket. Queue moves keep a legacy `_new` sweep for the same reason. - Rollback is unchanged: cancel/failure deletes the optimistic rows (now from the minted bucket) and `rollbackOptimisticTopic` already switches away. The old flow's tests moved with the contract: pending-intervention dismissal now lives in a real topic bucket (the `_new` bucket can no longer hold messages), and the queued-follow-up move asserts the legacy `_new` sweep. A new regression test pins the core invariant: mid-send, `activeTopicId` already equals the minted id sent as `newTopic.id`, the optimistic pair lives in the minted bucket, `_new` stays empty, and the id leaves `creatingTopicIds` once the server confirms. Verified end to end in a browser (MutationObserver over data-message-id): gateway first send, client first send, and the existing-topic second send show zero empty frames; the two e2e suites that caught the earlier remount regression (agent-scroll, agent-conversation-mgmt) pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c19240488a |
✨ feat: support proactive messenger push and notification channel settings (#17791)
✨ feat: support proactive IM notifications
|
||
|
|
68f4e84fe9 |
⚡️ perf(desktop): reduce cold-start critical path (#17811)
* ⚡️ perf(desktop): optimize cold startup * ♻️ refactor(desktop): rely on workspace package exports * ♻️ refactor(spa): isolate web-only routes * 🐛 fix(desktop): satisfy CI type checks |
||
|
|
d5eeb8e5f0 |
✨ feat(home): refine dashboard layout and rail (#17756)
* ✨ feat(home): refine dashboard layout and rail * 💬 chore(home): clarify task mode description * ✅ test: align local storage mocks with sync reader * 🐛 fix(home): restore main scrollbar gutter * 💄 style(home): refine activity rail control |
||
|
|
03d4b88a2b | chore: remove LOBE-XXX annotation markers from source code (#17689) | ||
|
|
93213a51fb |
⚡️ perf: make SPA startup demand-aware (#17577)
* ⚡️ perf(vite): make route chunk warmup demand-aware * ⚡️ perf(spa): preload routes through shared lazy loaders * ⬆️ chore(deps): align UI editor and Shiki versions * ⚡️ perf(pwa): cache lazy code on demand * ⚡️ perf(spa): reduce initial JavaScript dependency closure * ⚡️ perf(desktop): simplify renderer startup * ⚡️ perf: refine imperative modal loading boundaries * 🐛 fix(ci): restore production SPA startup * ♻️ refactor: share deferred PWA cache rules * 🐛 fix(ci): execute SPA copy entry with Node |
||
|
|
bd73f6b532 | 💄 style(settings): polish workspace settings pages (#17696) | ||
|
|
0a697e80b5 | 🐛 fix: restore image paste in browser runtime (#17644) | ||
|
|
0b5af01cef |
✨ feat(desktop): preview workspace HTML files (#17545)
* ✨ feat(desktop): preview workspace HTML files * ♻️ refactor(mime): unify MIME detection via @lobechat/utils/mimeType Fold the ad-hoc extension→MIME maps in desktop, CLI, and device-control into a single buffer-aware resolver. `resolveMimeType(path, buffer)` trusts file-type's magic-byte detection first, then falls back to mime.getType with a text-sniff downgrade so ambiguous extensions like `.ts` (video/mp2t vs TypeScript source) resolve correctly everywhere. * fix: resolve html preview ci failures |
||
|
|
3384fe491e | ✨ feat: add workspace topic comments (#17537) | ||
|
|
c8e589b98f |
✨ feat: support workspace-scoped notifications (#17587)
* ✨ feat: support workspace notification * 🐛 fix: scope single-notification mutations and context-key the inbox caches * 🐛 fix: mirror the notification deep merge in the optimistic store cache |
||
|
|
41c7bc948f | ✨ feat: expose client version metadata (#17571) | ||
|
|
b23b3f737c |
⚡️ perf(utils): reduce punctuation truncation probes (#17158)
Co-authored-by: aimi <298268675+alienslime@users.noreply.github.com> Co-authored-by: YuTengjing <ytj2713151713@gmail.com> |
||
|
|
f945c00d50 | ♻️ refactor(lint): clean up eslint-disable directives (#17206) | ||
|
|
1ced57852a |
✨ feat(topic): schedule a hetero agent run for later ("send in N hours") (#17127)
* ✨ feat(topic): schedule a hetero agent run for later ("send in N hours") Generalize the existing scheduled-topic machinery — until now a single-purpose mechanism for resuming a heterogeneous turn after a provider rate limit — into a kind-discriminated "deferred execAgent call", then add a user-facing deferred send on top of it. `topic.metadata.scheduledRun` now separates three concerns that were tangled into one rate-limit-shaped payload: - WHEN: a single top-level `runAt` gate, replacing the rate-limit `resetsAt` that doubled as the due check. It is required — an absent gate used to read as "due immediately", which is fatal for a user-chosen time. - LEASE: the `claim` lease, already kind-agnostic, is untouched. - WHAT: a `kind` discriminated union. `resume_after_rate_limit` keeps the old behavior; `delayed_start` is the new one. The dispatcher's scan / claim / clear loop is kind-agnostic; only "which execAgent call to make" is per-kind, behind a handler registry that the type forces you to extend when adding a kind. Continuations parked by the shipped version have neither field, so the reader upgrades them (`parseTopicScheduledRun`) and the due query keeps their old `resetsAt` gate alongside the new one. A migration would work too, but a scheduled run is cleared the moment it dispatches — the legacy shape drains on its own, and reading both costs less than backfilling live jsonb. A `delayed_start` persists its prompt as a real user message and points `scheduledRun.userMessageId` at it, so the pending turn reads as the user's own words in the topic instead of hiding in metadata — and that message stays the single source of truth for the prompt (the dispatcher reads it back, so editing a pending run is just editing the message). At dispatch it re-enters `execAgent` with `suppressUserMessage` and anchors the reply on that message: with no user row written, an absent `parentMessageId` would persist the reply as a second root and render it above the prompt it answers (LOBE-11489). UI (hetero composer only — a CLI agent is where deferring a run makes sense): - A hetero-only `+` menu holding "Send later" and the formatting-toolbar toggle. The agent Plus is not reused: attachments / web search / skills / gateway mode are all inapplicable here, and a menu of mostly-grey rows is worse than a small one. - Picking a time only *arms* the send; nothing is created until Send is pressed, so the send button stays the single commit action. The armed state shows as a chip next to the `+` that produced it, and times land on the hour. - The composer is cleared only once the schedule is persisted. Scheduling is one rejectable round-trip (a time that just went past, a failed request) and leaves no message row to recover the text from, so an up-front clear would lose the user's draft and attachments outright. - The pending turn carries a footer with the run time and a cancel action. Cancelling hands the text back to the composer and drops the topic, which existed only to hold that turn. Also fixes a latent bug in the existing dispatcher: a scheduled topic whose payload could not be dispatched was skipped without clearing, so it re-surfaced on every tick forever. It is now discarded back to `active`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(db): stop null-testing jsonb paths in WHERE — it crashes the prod engine `(metadata #>> '{a,b}') IS NULL` in a WHERE clause takes the engine backing production down outright: `rt_fetch used out-of-bounds`, SQLSTATE XX000, thrown before a single row is read. It is not a data problem — a table with zero matching rows crashes just the same — it is the query *shape*. `IS NOT NULL` and `IS DISTINCT FROM` on an arrow/path expression die the same way. Drizzle reports it as a bare `Failed query:`, with the real cause buried in the driver's `[cause]`. Same engine defect that took out `getLatestSpineMessageId` (LOBE-11376, #16693), now hit by a fresh query on a different table. `getDueScheduledTopics` (#17077, on canary since 07-12) has therefore crashed on EVERY cron tick since the day it shipped: the scheduled-run dispatcher died at step one, so no rate-limited heterogeneous turn has ever actually resumed. Only two topics ever entered `scheduledRun` and both have since moved on, so there is nothing stranded to recover. COALESCE the extracted value to a sentinel instead — equivalent semantics, a shape the engine survives. Three sites: - `getDueScheduledTopics`: the live crash. Both the runAt/legacy due gate and the claim-lease check are now COALESCE'd, and the claim's two-branch `or` collapses into one comparison ('' sorts before every ISO timestamp). - `getCronTopicsGroupedByCronJob`: `->>'cronJobId' IS NOT NULL`. No production caller today, so it has never fired — fixed before something connects to it. - `listTopicsForMemoryExtractor` / `countTopicsForMemoryExtractor`: `->>'userMemoryExtractStatus' IS DISTINCT FROM 'completed'`. These DO have production callers (the hourly user-memory extraction). Stock Postgres runs every one of these predicates happily, so the model tests were green throughout and no test on a real database can ever catch the next one. `jsonbNullTest.test.ts` guards the source shape instead — the only place this is catchable before prod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
fda97c698e |
✨ feat(topic): schedule resumable hetero continuation (#17077)
* ✨ feat(topic): schedule resumable hetero continuation * ♻️ refactor(topic): share metadata update schema * 📝 docs: sync Next.js agent rules * ♻️ refactor(device): consume shared working directory schema * 🐛 fix(topic): preserve failed scheduled continuations * ✅ test(hetero): mock base UI rate-limit actions * ✅ test(hetero): remove status guide component tests * 🐛 fix(topic): add scheduled status visual mapping |
||
|
|
94325a6311 |
🐛 fix(utils): handle non-ASCII text in browser base64 helpers (#16880)
encodeToBase64/decodeFromBase64 are documented as cross-platform, but the browser branch called btoa/atob directly. btoa throws InvalidCharacterError on any code point > U+00FF and atob returns a Latin1 string, so non-ASCII input (e.g. a UTF-8 ComfyUI Basic-auth credential built via createBasicAuthCredentials) failed in the browser while the Node path worked. UTF-8-encode before btoa and UTF-8-decode after atob so both environments behave identically. Co-authored-by: xiaweiwei67-stack <293320877+xiaweiwei67-stack@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: YuTengjing <ytj2713151713@gmail.com> |
||
|
|
a92e46e03d |
🐛 fix(utils): preserve valid surrogate pairs in sanitizeUTF8 (#16879)
sanitizeUTF8 stripped every UTF-16 surrogate code unit, destroying valid emoji and astral-plane characters (e.g. U+1F600, U+1D54F) rather than only the unpaired surrogates the comment describes. Add the regex 'u' flag so valid pairs match as one code point and are kept, while lone surrogates are still removed. This runs on RAG document chunk text in the async file router, so such characters were silently dropped from indexed content. Co-authored-by: xiaweiwei67-stack <293320877+xiaweiwei67-stack@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: YuTengjing <ytj2713151713@gmail.com> |
||
|
|
7e0993ecd5 |
🐛 fix(topic): sort sidebar by activity time to stop list jumping (#16868)
* 🐛 fix(topic): return activity time as updatedAt so sidebar sort matches server order The sidebar sorts topics client-side by the returned `updatedAt`, but the server ORDER BY uses `topicActivityAt` (latest message time, COALESCE fallback to the row's own updatedAt) while the SELECT returned the raw `topics.updatedAt` column. The two sort keys disagree, so newly-messaged topics fail to rise and status flips that bump the row updatedAt jump around before a refetch snaps them back. Return `topicActivityAt` from the three query branches so the client sort key matches the server order. * 🐛 fix(topic): sort sidebar by dedicated sortUpdatedAt to stop list jumping Return the latest message-activity time as a separate `sortUpdatedAt` field instead of overwriting the row's `updatedAt`. The sidebar sorts and groups by `sortUpdatedAt ?? updatedAt`, matching the server ORDER BY, so the two sort keys can no longer disagree and reorder the list. - server: query returns raw `updatedAt` + `sortUpdatedAt` (topicActivityAt) - client: sortTopics / groupTopicsBy* key on getTopicSortTime() - optimistic: send/regenerate + tmp-topic paths bump `sortUpdatedAt` - revert the isStatusOnlyWrite guard — status flips can't reorder anymore since the sort no longer keys on `updatedAt` LOBE-11543 |
||
|
|
310960d6a8 |
✨ feat(chat): persist working directory metadata (#16675)
* ✨ feat(chat): persist working directory metadata * ✨ feat(chat): wire topic worktree context * ✨ feat(chat): show worktree id and linked PR in topic sidebar Surface a compact second line under each topic row (by-project / flat / time / search views) carrying the topic's worktree directory name paired with its branch, plus the linked PR number chip. Renders only for topics on a linked worktree or with a linked PR, so plain source-checkout rows stay clean. Data comes from the persisted workingDirectoryConfig.git — no extra probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 💄 style(chat): enlarge worktree/PR meta line to 12px with clearer spacing Bump the topic-row worktree id + linked-PR second line from 11px to 12px, widen the icon/label and segment gaps, lift icon size to 13px, and add a small top margin so the line reads comfortably instead of cramped. * ✨ feat(chat): topic worktree/PR hover detail card + GitHub PR state markers Replace the inline worktree/PR second line on topic rows with a Codex-style hover detail card: hovering a topic reveals a card on the right with repo / branch / worktree / linked PR / CI status plus a chat-branch note. The row itself stays clean (single line). Mark GitHub PR lifecycle state (open=green, merged=purple, closed=red) in two places: the topic row's leading icon (below running/failed/waiting, above the idle default) and the card's PR row (colored state word + title + number). State derives from mergedAt/state (merged checked first). CI status is colored too (passed green / failed red / running amber / none grey). Data comes from the persisted workingDirectoryConfig — no extra probe. Adds topic i18n (metaCard.*) in en source + en-US + zh-CN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🐛 fix(chat): anchor hetero CLI session cwd to the source repo, not the worktree Claude Code / Codex store sessions per-cwd. Previously a hetero run used the effective (worktree) path as its cwd + resume key, so switching worktree started a brand-new CLI session and silently dropped the conversation context. Anchor the session cwd to the SOURCE repo instead: switching worktree now only records `git.activeWorktree` in metadata and never changes cwd or resets the session — so cwd and sessionId stay consistent and the context is preserved. Only a change of repo invalidates the session (keeps the reset warning for that). Non-hetero agents keep running in the effective/worktree cwd. Robust against production too: resume rides the existing scalar `heteroSessionId` + source `workingDirectory`, so it works even before the new metadata fields are persisted server-side. Verified end-to-end with real Claude Code (a worktree switch mid-conversation resumed the same session and remembered prior context). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🐛 fix(chat): fix two working-directory metadata edge cases - hetero completion write: prefer the topic's CURRENT workingDirectoryConfig over the run-start captured one, so GitStatus's freshly-persisted git.github (branch/PR/CI) isn't dropped on completion. - workspace scan: upsert on the matched source entry's path, not boundCwd. The cache lookup can match a source entry by its effective (worktree) path, so a worktree-bound scan was prepending a bare worktree recent and losing the source/worktree metadata the picker relies on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4f894c6de6 |
💄 style: token B rollover, ux skill restructure, agent breadcrumb & approval card (#16587)
* 💄 style(usage): roll token usage values over to B beyond 1000M formatUsageValue capped at M, so large token counts rendered as "10285.7M" instead of "10.3B". Add a B tier at the 1000M boundary and unify the duplicated formatToken helper in TokenProgress onto it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(tool-execution): return a recoverable error for unknown builtin API A hallucinated apiName threw outside the try/catch, surfacing as an uncaught failure instead of a tool result. Return a structured UNKNOWN_API error listing the tool's real APIs so the model can recover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ⏪️ revert(skill): restore the ux skill and add a number-abbreviation rule #16560 deleted the ux skill (SKILL.md + references/design-values.md) and stripped its pointers from the react skill. Restore all three (latest pre-deletion content, still pointing at DESIGN.md for Voice & Content), and add §1.5 "Abbreviate large numbers, roll the unit over" documenting the K/M/B/T convention behind the formatUsageValue B-tier fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✨ feat(agent): unify agent page headers with a shared breadcrumb Extract the topic header's inline breadcrumb into a reusable AgentBreadcrumb (`<AgentName> › <Section>`, agent name links home) and reuse it across the agent profile (助理档案) and usage & cost (用量与成本) pages so sibling pages read consistently. Identity/location (breadcrumb) leads the profile header, separated from the state tags that follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(approval): widen the approval card and pin the ask-user footer Widen the global approval card from 520px to 640px so option-heavy requests (e.g. PR-scope questions) have room. Wire actionsPortalTarget through to custom interventions and have AskUserQuestion portal its countdown + skip / submit row into the card's fixed footer slot, so the actions stay pinned below the scroll area instead of scrolling away with the content. Also migrate its Button / Tabs imports to @lobehub/ui/base-ui. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📝 docs(skill): split ux checklists into per-module references & clarify DESIGN.md boundary - Split the ~385-line ux SKILL.md into per-interaction-module reference files (read / edit / act / feedback / grow); SKILL.md is now a lean index (values + principles + module links + one-screen quick checklist). - Reformat each section into prose (rationale + ✅/❌ examples) followed by a concise, one-line-per-item checklist, instead of fat bullets that bundled rule + explanation + example in a single item. - Add an explicit "DESIGN.md vs ux skill" boundary to both files: DESIGN.md = design system (tokens, components, voice); ux skill = interaction behavior. DESIGN.md now points interaction-detail work at the ux skill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(tool-execution): source UNKNOWN_API hint from the manifest The recovery hint listed available APIs via `Object.keys(runtime)`, which only sees own enumerable properties. Almost every server runtime declares its APIs as class prototype methods (`async sendMessage() {}`), so the hint collapsed to `Available APIs: .` — actively telling the model the tool has no usable APIs, the opposite of the intended self-correction. Read the declared API names from the tool manifest (the authoritative source), falling back to a prototype-chain walk when no manifest is available so the list is never empty when the runtime has callable methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 📝 docs(skill): add ux rule for pinning actions outside the scroll region Capture the approval-card footer-pinning behavior as ux §3.3 "Pin actions & status outside the scroll region" (entity lifecycle shifts to §3.4), and mirror it in the Quick review checklist under Act. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📝 docs(skill): require a terminal failure path for every loading state Add ux §4.2 "Loading must be able to fail — timeout → error + retry": every loading state needs a bounded-wait/error terminal state with a Reload/Retry action, never an infinite spinner. Mirror it in the Quick review checklist under Feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📝 docs(skill): add ux-audit skill and land homepage audit findings Add a new `ux-audit` skill: a repeatable, Designing-Interfaces-benchmarked audit of one surface at a time, with a pattern catalog, a severity rubric, an output template, and the home-page audit as a worked example. Cross-link it from the ux skill. Land the reusable findings from that audit into the ux checklists: - read.md §1.7 Live / polling streams (new-item indicator, manual refresh, no reorder under the user, failed-refresh ≠ empty) - edit.md §2.1 strengthened (durable draft, survives reload) + §2.2 stable input affordances (static placeholder, no hidden clickable content) - feedback.md §4.2 strengthened (init-flag-gated-on-success → permanent skeleton failure mode) - mirror the new rules into the SKILL.md Quick review checklist Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(approval): pass rendered icons to base-ui Button in AskUserQuestion base-ui Button's `icon` prop is a ReactNode (not a component ref like the old antd-based wrapper accepted), so `icon={X}` / `icon={Send}` failed type-check (TS2322). Wrap them in `<Icon icon={…} />` to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✅ test(profile): stub AgentBreadcrumb in the profile Header test The profile Header now renders AgentBreadcrumb, which reads agentSelectors.getAgentMetaById / builtinAgentSelectors.inboxAgentId. The test mocks the agent store with only a subset of selectors, so the breadcrumb crashed with "getAgentMetaById is not a function". Stub the breadcrumb like the header's other child components — the test covers the export menu, not the breadcrumb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
be0255b746 |
🔨 chore: update dependency baseline (#16465)
* 🔨 chore: update dependency baseline * 🔨 chore(cli): update dependency baseline * 🔨 chore(desktop): update dependency baseline * 🔨 chore: fix dependency baseline blockers * 🔨 chore: update TypeScript and Next baseline |
||
|
|
dee95f5419 |
💄 style(tool): compact single-row Linear list render (#16359)
Render Linear list_* entities as one tight row each: title on the left, relative "updated" time on the right. Drop the chunky per-field grid, hide meaningless UUID ids (human ids like LOBE-123 stay), and show the update time as locale-aware relative time (e.g. "更新于 3 天前") with the full timestamp in the title tooltip. Adds a `fromNow` util, the `builtins.linear.render.updatedAt` i18n key (en-US + zh-CN), and a devtools RenderGallery fixture so the Linear render is previewable. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
479514d0ff |
✨ feat: support Bedrock API key auth (#16284)
* ✨ feat: support Bedrock API key auth * 🐛 fix: guard Bedrock auth mode changes for read-only users * ✅ test: type MessageContent markdown mock args * 🐛 fix: preserve Bedrock API keys without random selection * ✅ test: type Bedrock runtime inspection in ModelRuntime tests * 🐛 fix: support Bedrock API key rotation securely |
||
|
|
724659f10d | ✨ feat: support contextual model pricing (#16135) | ||
|
|
ef09457e63 |
✨ feat(topic): persist topic unread as a backend status (#16056)
* ✨ feat(topic): persist topic unread as a backend status Move the "unread completed generation" indicator from a client-only Zustand Set to a persisted `topics.status = 'unread'`, so it survives reload and syncs across devices. - add 'unread' to the topic status enum / type / updateTopic zod / sort rank - compute cross-agent unread counts server-side on the sidebar agent list (SidebarAgentItem.unreadCount) so the home badge stays accurate for agents whose topics aren't loaded on the client - markTopicUnread (formerly markUnreadCompleted) writes status 'unread'; markTopicRead (formerly clearUnreadCompletedTopic) flips it back to 'active' when the user opens the topic - partition the terminal status write by (viewing, succeeded) so the background completion's 'unread' write never races the 'active' write - derive unread selectors + sidebar status bucketing from topic.status Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(topic): clear unread on route hydration; don't mark parked/cancelled runs unread Address two review findings on the persisted unread status: - Route hydration (reload / deep link / notification) sets activeTopicId directly instead of going through switchTopic, so the persisted 'unread' was never cleared while the user was already reading. Add useClearActiveTopicUnread — once the active topic appears in a loaded bucket as 'unread', mark it read. Wired into agent + group ChatHydration. - A gateway agent_runtime_end with reason 'interrupted' / 'waiting_for_async_tool' is a cancel / park, not a completion. Gate markTopicUnread on isCompletedRuntimeEnd(reason) and derive onSessionComplete's `succeeded` from the same predicate, so aborts/parks clear back to 'active' instead of persisting as an unread completion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ♻️ refactor(topic): hoist topic status enum into @lobechat/types + add TopicModel tests - extract the inline status z.enum in the topic router into a shared chatTopicStatusSchema / TOPIC_STATUSES source of truth in @lobechat/types - derive ChatTopicStatus from the schema so the type and validator stay in sync - add TopicModel unit tests covering CRUD, status filtering/ordering, triggers, metadata merge, duplicate, batchMoveToAgent and memory-extractor queries Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f6db1361ee | ✨ feat(agent): show topic sidebar status indicators (#15739) |