* ✨ feat(task): record assignment changes in the task activity feed
Assigning a task to a member or an agent only rewrote a column on `tasks`,
so the detail page's activity feed had no way to show who moved it or when
— a reader saw "created the task" and nothing else, however many times the
task had changed hands.
Every write path already funnels through the `task.update` procedure (the
member/agent pickers, the `editTask` builtin, the CLI), so the diff between
the pre- and post-update assignee columns is taken there and appended to
`task_activities`. Only a slot that actually moved is written, so re-saving
the same assignee stays silent. Unlike the assignment notification beside
it, the write is awaited rather than deferred: the client refetches the
detail as soon as the mutation resolves, and a row written after the
response would miss that refetch.
`updateSchema` gains an `actorAgentId` used solely for attribution, so an
agent reassigning through its tool is recorded as the agent rather than the
human whose session it borrowed.
On the render side `ActivityAuthor` is extracted so the actor and the
assignment target share one avatar/name/tag treatment, and `AssignmentRow`
reads as "<actor> assigned the task to <target>" / "<actor> removed the
agent". A target whose account no longer resolves keeps a stub author and
renders as deleted, because collapsing it to null would turn a reassignment
into a removal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🌐 chore(i18n): sync generated locales
Runs `bun run i18n` so the 16 generated locales pick up the task assignment
activity strings instead of falling back to English until the daily workflow
catches up.
The run also flushes locale drift that had accumulated for other namespaces
(auth / device / modelProvider / models / providers) and realigns two
hand-edited en-US / zh-CN values with their source in
`packages/locales/src/default/chat.ts`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🔒 fix(task): make assignment attribution unforgeable and atomic
Addresses three findings from review on the assignment activity log.
**Attribution came from the request payload.** `updateSchema.actorAgentId` was
public input, so any authorized client could name an arbitrary agent and have
the durable feed claim that agent made the reassignment — while suppressing the
real user. The field is gone; the agent now rides `AuthContext.actingAgentId`,
set only by a server-side `createCaller` from an already-authorized run
context, following the contract `spendOrigin` already established.
**The diff was read outside the write.** The previous assignee was read by the
router before the update and recorded after it, so concurrent reassignments of
one task could both observe the same origin: a racing A→B and A→C persist A→B
then B→C while the log claims A→B and A→C, losing the middle state, and the
rows could be ordered differently from the writes. `updateWithAssignmentLog`
now derives the previous value from a `FOR UPDATE` read and appends the events
in the same transaction as the update.
**One writer bypassed the recorder.** `TaskRunnerService` persists the inbox
agent when an unassigned task first runs, straight through `taskModel.update`.
The assignee chip visibly changed with nothing in the feed to explain it; that
write now goes through the same path, with neither actor column set since
nobody requested it — the feed renders those as the system.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🌐 fix(task): make assignment attribution reach the router, and let locales order the sentence
Round-2 review follow-ups on the assignment activity log.
**The attribution fix was inert.** The caller built with `actingAgentId` in the
runtime factory is replaced by `ensureModels()` before any exported method
runs, and the replacement omitted it — so every agent-driven reassignment was
still recorded as the session user. The previous test missed it by building
`createTaskRuntime` directly with a ready-made caller, never exercising the
registration lifecycle; the new test drives `taskRuntime.factory` and asserts
on the caller the wrapped method actually uses.
**The row could not be translated.** Actor, verb and target were three DOM
nodes in a fixed order, so a verb-final language could not put the target
before the verb — Japanese rendered as a complete sentence with the target
stranded after it. The four verb strings are now full sentence templates with
`<actor/>` / `<target/>` placeholders rendered through `<Trans>`, so each
locale decides where the participants go. The generated locales had to be
dropped and retranslated: the keys kept their old fragment values otherwise,
since the script only fills in missing ones.
**The agent prompt claimed false provenance.** An actorless row is the runner's
system fallback, which the feed labels "System" but the prompt rendered as
"someone".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🐛 fix(task): tell absent, invisible and unnamed participants apart
Round-3 review follow-ups. All three findings come from the same mistake:
collapsing distinct identity states onto one fallback.
**An invisible agent was credited to the system.** `getAgentAvatarsByIds`
filters by `ownership()`, so another member's private agent does not resolve
for the current viewer. The actor lookup returned `undefined`, and both the row
and the prompt reserve an absent author for the system — so an agent's
reassignment was durably shown as a system action. The actor now keeps a stub
carrying `unresolved: true`, as the assignment target already did.
**A live participant could borrow a deletion label.** `users.full_name` and
`agents.title` are both nullable, so a resolved row with an empty name fell
through `||` onto "System" or "a deleted agent". Display names now fall back to
the other column each query already returns (`title || name`,
`fullName || username`), and a resolved-but-nameless participant renders as
"Unnamed" rather than as gone.
**Two formatters dropped the event.** The `viewTask` tool prompt and the CLI
task detail have their own activity branches and neither knew about
assignments, so the event vanished on those surfaces. Both render it now,
through a shared `assignmentParticipantLabel` on the prompt side; the CLI keeps
a three-line local copy rather than take a dependency on `@lobechat/prompts`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* ⚡️ perf(task): show the assignment activity row in the same beat as the chip
Picking a new assignee flipped the chip instantly (that dispatch was already
optimistic) but the activity row explaining it only appeared after two
serial round-trips: the mutation, then the full task-detail refetch — a
fifteen-step aggregation that alone runs one to three seconds against a
remote database. The chip said "done" while the feed stayed silent.
The row is now synthesized in the same optimistic dispatch. The picker hands
over the chosen item's display metadata, since it is the one place that
already holds it — the store deliberately does not resolve members itself,
that source being a business-layer hook. The actor is the signed-in user;
with no known identity no row is invented.
Only a slot that actually changes produces a row, mirroring the server's own
rule, and the refetch that follows the mutation replaces the whole activity
array, which retires the synthetic rows with no reconciliation. A failed
mutation is rolled back by that same refetch. Comments share the identical
two-round-trip lag and are left as they were.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* ⚡️ perf(task): show a sent comment in the feed before the round-trip returns
Comments had the same two-round-trip lag the assignment row just lost: the
post, then the full task-detail refetch, before anything appeared. Sending
now synthesizes the row in the same dispatch, authored as the signed-in user
and carrying the topic it replies to, so it lands in the feed the moment the
button is pressed.
Attachments are deliberately left to the refetch — their metadata only exists
once the server has resolved the file ids. A comment posted on an agent's
behalf through the client-first runtime is also left to the refetch, since
the store has no agent identity to attribute it to.
While the synthesized row is on screen its edit / delete menu is withheld —
the server has never seen that id, so either action would only surface an
error. A failed post is rolled back by the same server-truth refetch the
assignment path uses, and the failure still reaches the caller.
The two builders now share one module and one id prefix, so a renderer has a
single predicate for "not yet persisted".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* ✨ feat(task): log status, priority and automation changes made by a person
The activity feed now explains every property a person can change from the
task's property bar, not only the assignee. A status pick, a priority pick and
turning automation on or off each leave a row: "<actor> changed the status to
<tag>", "set the priority to <tag>", "set automation to <summary>".
Only person-made changes are recorded — those made from the pickers or by an
agent acting through its tool. The runner, lifecycle and watchdog transitions
(backlog → running → completed) are deliberately not: the run row already
carries them, and a duplicate line per run would be noise. The policy is not a
flag; it is which writer a caller uses. Person-made edits go through the new
`TaskModel.updateWithLog`, the system transitions keep the plain writers.
`updateWithLog` replaces the per-property methods with one transactional
diff over a tracked column set (assignees, status, priority and the four
automation columns), so adding a tracked field is one list entry. The four
automation columns fold into a single event carrying before / after
snapshots: switching a schedule on rewrites mode, pattern and timezone in one
save, and that is one decision, not three.
All three task-detail formatters (page prompt, `viewTask` tool prompt, CLI)
render the new event, through a shared `formatPropertyValue` on the prompt
side.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* ✨ feat(task): fold a burst of edits to one property into a single feed entry
Fiddling with a property — priority High → Low → High inside a minute —
produced one line per hop, where an issue tracker shows "moved from A to C"
and shows nothing when the value ended up back where it started.
Collapsing happens at read time, in the detail assembly, so the rows in
`task_activities` stay append-only: the hop-by-hop history is still there for
audit or a "show all" view, and every consumer of the detail (page, `viewTask`
prompt, CLI) gets the same folded view. A run is one property by one person
with no gap over the window; it keeps the newest row's time (when it settled),
the first row's starting value and the last row's ending value. Two people
never merge — A moving it and B moving it back are two decisions, not a
cancelled one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 💄 style(task): say where a property moved from, not only where it landed
"changed the status to In Review" hides the half of the story a reader
actually wants once a burst of edits has been folded into one entry: an
entry can now span several hops, and the starting value is what says how far
the property moved. Status, priority and automation rows read "from A to B";
automation keeps its one-sided forms for turning on ("set automation to")
and off, since there is no other side to name.
The two reworded keys had to be deleted from the generated locales and
retranslated — the script only fills in missing keys.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🐛 fix(task): surface every person-made property change in the feed at once
Three findings from driving the real UI:
**Automation changes never reached the feed.** `setAutomationMode` deliberately
does no post-save refetch — an async SWR write could land after the user's
next toggle and clobber it — so the row the server appended only showed up on
the next unrelated refresh. The row now rides the same optimistic patch as the
automation fields, inside the same engine transaction, so a failed save rolls
it back with them.
**Status and priority rows arrived a round-trip late.** Only assignment and
comment rows were synthesized; the other two waited for the detail refetch,
which against a remote database is the same lag the assignee row was fixed
for. Both now ride the optimistic dispatch that flips their chip.
**Priority read as "from ··· to !".** The priority tag alone is an icon; in a
sentence it is noise. The level's name now rides along.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🐛 fix(task): attribute client-runtime edits, tighten activity collapse and rollback
- update/updateStatus accept an ownership-checked actorAgentId for the
client-first runtime; the server-side actingAgentId still wins
- collapse only extends a run when each hop starts where the last ended
- optimistic property rows fold client-side; schedule edits get a row
- addComment drops its synthesized row locally when the send fails
- payload.actorKind keeps a deleted actor from reading as the system
- task detail reads a bounded window of recent activity rows
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🐛 fix(task): log cascades and rolled-back starts, quieter activity rows
- collapse: any edit by someone else ends other actors' open runs
- updateStatusCascade logs one status row per family member the
confirming person cascaded; the router passes the actor
- a person-made start rolled back after the heartbeat tick fails logs
the undo too, so no orphan row survives the restore
- activity rows read like an issue tracker's: 16px mark, plain names,
values as words instead of chips, agent chip dropped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 🐛 fix(task): lock cascade origins, batch their rows, widen the schedule value
- cascade reads each task's leaving status under the row lock and writes
the family's activity rows in one INSERT
- the automation value includes the execution cap and its labels show
timezone and cap, so a cap-only or timezone-only edit is visible
- a failed status transition drops its synthesized row locally before
the best-effort refetch
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 💄 style(task): lay the activity feed out as a timeline
Compact rows lead with a type mark on a shared rail instead of the
actor's avatar, read as one sentence in one tone, and sit tight together;
cards still break the rail.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 💄 style(task): faces for people rows, property icons for property rows
The rail mark is the actor's avatar on created / assignment lines and the
property's icon (three-bar priority, status dot, timer) on change lines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 💄 style(task): priority rows use the app's solid priority bars
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
* 💄 style(task): centre the activity rail under the cards' avatar column
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 🐛 fix: keep the resource tree and connector list in sync with their scope
Three scope/staleness fixes that share the same shape — a client store
holding on to rows the current context no longer owns.
- LOBE-13804: `useFileItemDropdown` refreshed the tree with the explorer's
current folder, but the sidebar navigates into a folder on click, so
"open it, then delete it" refreshed the deleted folder while the parent
list kept the row until a reload. Tree store gains `dropNodes`, which
drops the rows where they actually live, purges the removed subtree's
caches, and revalidates only the parents that were hit. Explorer bulk
delete now syncs the tree too, and deleting the folder the explorer sits
in steps out to the parent instead of stranding a dead route.
- T-392: connector fetches wrote their result without checking the scope
they were issued in, so a personal `list` still in flight could land
after the workspace switch and pin the wrong list for the session.
Capture `getActiveWorkspaceId()` before the request and discard a stale
response, mirroring the builtin slice.
- LOBE-13805: drop the permanently disabled workspace settings reset entry
and its locale keys.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHwD8Ev5t7y3xDiBTc9m8K
* 🌐 chore(i18n): run the translation workflow
Regenerated after the rebase. Backfills keys other iterations left
untranslated and drops entries whose source keys are gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHwD8Ev5t7y3xDiBTc9m8K
* 🐛 fix: step out of a deleted folder's whole subtree, not just the folder
Review follow-up on the sidebar delete fix.
`handleDeleted` compared the deleted row against the explorer's selection
directly, so it only fired when the explorer was parked in the folder being
deleted. Deleting an *ancestor* — the sidebar renders the whole path, so
that is one click away — left the explorer exactly where the guard exists to
prevent: an empty list under a breadcrumb naming a folder that no longer
exists. Opening a document inside the deleted folder had the same shape.
Adds `hierarchySubtreeHoldsSelection`, which walks the loaded subtree and
answers whether the selection sits anywhere inside it. A selection under a
folder the sidebar never expanded is invisible to the walk and the explorer
stays put, matching how the rest of the tree treats folders it has not
fetched.
`onDeleted` now fires before `dropNodes` rather than after: the purge forgets
the subtree synchronously, so a caller that has to inspect what is about to
disappear could not see it. The fallback parent key is captured before the
callback runs, so the ordering carries no other meaning.
Also covers the two behaviours that shipped untested — the explorer's bulk
delete syncing the tree, and the folder-refresh path when the deleted set is
only known to the server — and documents why a discarded connector response
deliberately leaves the init flag alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHwD8Ev5t7y3xDiBTc9m8K
* 🐛 fix: don't yank the user back when a slow delete finally lands
Codex review follow-up (P2).
`handleDeleted` read the selection and library from the closure the context
menu captured when it was rendered. The delete is async, so a user who opens
another folder — or another library — while the request is still in flight
was navigated back to the deleted folder's old parent once it resolved,
after they had already moved on.
The decision now moves into `resolveDeletedFolderRedirect`, a pure function
over a snapshot the caller reads at call time: the component keeps the live
library and selection in a ref updated each commit, and skips entirely when
the row has unmounted, which is itself proof the user navigated away.
Making it pure also lets the delayed-delete case be tested directly, which
the component-level logic could not be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHwD8Ev5t7y3xDiBTc9m8K
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 🐛 fix: improve forbidden error handling and agent list actions
* 💬 i18n: sync locale translations
* ✅ test: unshadow server label in taskless acceptance link test
🌐 i18n: backfill locale translations for auth, modelProvider, notification, subscription and verify
Generated with `bun run i18n` against the current default sources — fills
keys recently added on canary (auth agreement flow, notification center,
subscription, verify report vocabulary) across all non-hand-written locales,
and drops the removed `report.scope.focus` key.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 🐛 fix(provider): use base-ui modal so delete confirm stacks above the config dialog
Closes#15836
* 💄 style(provider): split delete confirm into short title and description
* 🌐 chore(i18n): sync delete confirm title/description across all locales
* ✨ 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
* ✨ feat(skillStore): add "Want more skills?" prompt with feedback integration
- Add WantMoreSkills component below CommunityList
- Extend useFeedbackModal to support preset initial values
- Update FeedbackModal to accept and display initial form values
- Add i18n translations for zh-CN and en-US
Closes LOBE-4163
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* 🔧 fix(skillStore): show skill request prompt at list end instead of fixed footer
- Move WantMoreSkills to VirtuosoGrid Footer, shown only when list ends
- Update text to "已经到底了,未找到所需技能?提交申请 →"
- Use Typography.Link for hyperlink style on action text
- Update feedback form template with structured format
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: initial values not set
* chore: remove skill install banner shadow
* chore: use same list height
* chore: update i18n files
* fix: klavis disconnected style
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* 🐛 fix: show notification when file upload fails due to storage plan limit
Previously, when file storage exceeded the plan limit, the TRPC
middleware threw a FORBIDDEN error that was silently swallowed by
the upload components with no user feedback. Now `uploadWithProgress`
catches this specific error and displays a notification guiding
users to upgrade or free up space.
* 🌐 chore: run i18n for upload storage limit error message
* 🌍 i18n: update model descriptions across multiple languages
Added new model descriptions and improved existing ones in Arabic, Bulgarian, German, French, Italian, Japanese, and Korean locales. This update enhances the clarity and detail of model capabilities, ensuring better user understanding and accessibility.
* refactor ollama pull flow and model service
* fix key vaults error
* refactor ollama model fetcher
* improve auto refresh for models
* fix error detail
* fix model fetch
* fix model fetch
* fix model checker
* fix model client fetch issue