mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
8fbb091050
* ✨ 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>