mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-22 17:49:48 +02:00
agent/lambda/768b92e0
303 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b2209d700 |
fix(properties): toast on failed board drag to a property column
Property-column drags rolled the card back silently on failure; mirror the status/assignee drag path (use-issue-surface-actions) so the snap-back is explained (clean-room review F3, drag half). Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d49d309489 |
Merge remote-tracking branch 'origin/main' into agent/lambda/768b92e0
# Conflicts: # packages/core/realtime/use-realtime-sync-ws-instance.test.tsx |
||
|
|
8f92b5fdeb |
feat(search): add fold/unfold all comments commands to the command palette (#5417)
On an issue page, Cmd+K now offers Fold All Comments / Unfold All Comments. Folding collapses every thread card via the persisted comment-collapse store; unfolding also expands resolved threads, whose session-only expand state moves from issue-detail useState into a new core resolved-expand store so the palette can drive it (MUL-4763). Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c402b91c1d |
fix(properties): harden concurrency and cache coordination from clean-room review
Backend (MUL-4762 F1/F4/F5): - withPropertyLock: pg_advisory_xact_lock helper; definition create/update and value writes now serialize config-vs-value and cap-vs-insert races (workspace-level 'props:' lock + per-definition 'prop:' lock, ordered). - propertySortExpr degrades archived definitions to position sort. Frontend (F2/F3/F6): - onIssuePropertiesChanged invalidates plain assignee-group caches too. - Property value mutations cancel list refetches in onMutate and roll back only the touched key against the current bag (concurrent WS writes to other keys survive a failed write). - useUpdateIssue reconcile drops the stale properties bag from the server snapshot; the property pipeline owns that field. - Surface controller passes persisted property filters/sorts through until the catalog query settles (cold cache no longer strips them). Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5999eabd92 |
fix(views): stop showing backfilled attribution as a warning (MUL-4768) (#5421)
* fix(views): stop showing backfilled attribution as a warning (MUL-4768) The transcript/activity AttributionBadge colored the "on behalf of <name>" chip yellow (text-warning) whenever attribution.precise === false. That flag is the backend's attribution-*coverage* health bit — owner_fallback, backfill, and unattributed all fail it — but coverage is an ops metric, not a reader-facing signal. A backfilled attribution names a human the same waterfall resolved, just retroactively, so it is confident; rendering it identically to a genuine owner_fallback guess made a correct "on behalf of Bohan" read like an error. Fire the cautionary tone only for a fallback guess (any non-precise source except backfill). Keeping the precise === false base means a future unknown degraded source still warns (fail-safe). The backfill nuance stays in the tooltip. Co-authored-by: multica-agent <github@multica.ai> * docs(views): correct backfill wording in AttributionBadge (nit MUL-4768) Review nit: describing backfill as "confident / same waterfall resolved" overstated the backend contract, which defines backfill as a historical, non-realtime, non-compliance-grade source. Reword the docblock, the tone rationale, and the test note to the accurate framing: backfill does not mean the displayed name is wrong (so no warning tone), but its historical origin is still preserved in the tooltip and the raw source field. Comments only; no behavior change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
09f69dfa05 |
refactor(attribution): drop on-behalf badge from execution log rows (MUL-4766) (#5419)
The "on behalf of <member>" attribution chip on each execution-log row added visual noise to the dense run list. Remove it from both the active and past run rows and restore the original layout. The attribution stays discoverable where it belongs: the task transcript header and the agent detail page's recent-work list still render AttributionBadge. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0276704323 |
fix(attribution): hide unattributed chip when a task has no responsible member (MUL-4765) (#5418)
The Human Attribution work (MUL-4302) showed a yellow "No responsible member" warning chip in the execution log and working-status surfaces whenever a task had no resolved responsible member. An unassigned run is a normal state, not something to flag, so the badge variant now renders nothing in that case — matching the avatar variant, which already stayed silent. Removes the now-dead `unattributed` locale string across all bundles. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fac8d772f |
feat(attribution): Human Attribution Phase 1 (MUL-4302) (#5150)
* feat(attribution): Phase 1 foundation — provenance schema + resolver (MUL-4302)
Human Attribution, Phase 1 (地基) first increment. Every agent run must be
traceable to exactly one accountable human AND record at which waterfall level
that human was resolved, so a NULL originator can be told apart from a genuine
'no human in the chain'.
- migration 149: add originator_source (waterfall label) + delegation/retry/
rerun/rule-version lineage + kind-tagged trigger evidence to agent_task_queue.
No FK, no cascade, no CHECK on the source enum (MUL-4302 §7); nullable ADD
COLUMNs = fast metadata-only change on the hot queue table.
- internal/attribution: the accountable-human vocabulary (Source, EvidenceKind,
TriggerKind) + pure, unit-tested classification rules (ClassifyComment/
ClassifyDirect). No DB, no authorization — provenance labeling only.
- service: attributionFor{IssueTask,TriggerComment} gather facts and delegate to
the pure classifier; the legacy originator resolvers now delegate here so
there is one source of truth. originator_user_id's VALUE is unchanged, so the
Composio-overlay and canInvokeAgent A2A authorization boundaries are
byte-for-byte preserved (MUL-4302 §1.3).
- enqueueIssueTask / enqueueMentionTask stamp originator_source + evidence;
CreateRetryTask carries the parent attribution forward and records
retry_of_task_id so retry and manual rerun stay separable (MUL-4302 §5).
Verified: go build ./..., go vet, gofmt clean; new attribution unit tests +
enqueue stamping integration test green; existing resolve_originator tests
unchanged.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): split accountable_user_id from originator, close enqueue bypasses (MUL-4302)
Phase 1, per Bohan's decision on the MUL-4302 thread: audit and authorization
answer different questions and get different columns.
- Migration 151 adds agent_task_queue.accountable_user_id (no FK, no cascade).
Authorization keeps reading ONLY originator_user_id (canInvokeAgent A2A gate,
Composio overlay); audit/UI/usage read accountable_user_id + source + evidence.
- Invariant (finalizeAttribution, single chokepoint + §11 tests): originator
non-null ⟹ accountable equals it. The two diverge only when originator is null
(autopilot / degraded fallback), which is the deferred rule_owner/owner_fallback
increment; this lands the column + mirror-write so that split has a home.
- Close the NULL-source enqueue bypasses Elon flagged: chat, quick-create,
deferred-fallback and run_only-autopilot now stamp originator_source + evidence
(+ accountable where a human exists). Autopilot stays unattributed until the
rule-version snapshot table lands, but is no longer a silent NULL-source row.
Retry inherits accountable_user_id like the rest of the attribution lineage.
- Fix assign/promote attribution (§4): a member who assigns/promotes an existing
issue is now the accountable human (and, by the invariant, originator) ahead of
the issue creator. Threaded as an OPTIONAL actor override, so comment/rerun/
autopilot paths keep today's resolution and create-with-assignee (creator ==
actor) is unchanged. The squad leader gate already judged the same member.
Also merges origin/main: renumbers the attribution migration 149→150 (main took
149 for issue_origin_agent_create) and folds agent_create into ClassifyDirect's
origin inheritance.
go build/vet/gofmt clean; attribution unit tests + service stamp/actor tests +
handler suite pass on a fresh DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* docs(attribution): fix accountable NULL semantics + close chat/quick-create evidence boundary (MUL-4302)
Addresses Elon's 2nd-round review on PR #5150 (pre-merge doc/evidence items):
- Migration 151 no longer overclaims NULL. accountable_user_id is NULL not only
on pre-migration rows but on NEW rows whose audit source resolved no human yet
(run_only autopilot writes originator_source='unattributed' with NULL
accountable until rule_owner lands). Header + COMMENT ON COLUMN reworded so a
schema reader does not misjudge the invariant.
- Chat now uses the UNIFORM evidence pair (kind=chat, ref=chat_session_id), like
autopilot_run/issue_assignment, instead of relying only on the dedicated
chat_session_id column — new EvidenceChat kind. Added a service test asserting
chat stamps direct_human + chat evidence.
- Quick-create is documented as the ONE intentional no-antecedent-row path: no
comment/issue/session/run exists at enqueue time (the run creates the issue), so
trigger_evidence_kind/ref stay NULL while the human rides originator/accountable
and source is direct_human — not a NULL-source bypass.
No authorization behavior change. attribution + service + handler suites pass on a
DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): renumber migrations 150/151 → 157/158 after merging main (MUL-4302)
main's #5162 ("unblock release migrations") renumbered the chat migrations and
took 150 (agent_task_coalesced_comments) and 151 (chat_read_cursor), colliding
with this branch's attribution migrations. Renumber them above main's new highest
(156) so TestMigrationNumericPrefixesStayUniqueAfterLegacySet passes:
- 150_agent_task_attribution → 157_agent_task_attribution
- 151_agent_task_accountable_user → 158_agent_task_accountable_user
Fixed the internal "migration 150" references in 158's header to 157. Migrations
apply cleanly through 158 on a fresh DB; migration lint green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot rule_owner — accountable = rule version publisher (MUL-4302)
Implements rule_owner (MUL-4302 §3.4), the first attribution source where the
accountable human diverges from the (NULL) authorization originator.
- Migration 159 adds the append-only autopilot_rule_version snapshot table (no FK,
no cascade); migration 160 adds its CONCURRENTLY lookup index.
- Write-on-publish: CreateAutopilot appends v1 (publisher = creator); UpdateAutopilot
appends a new version when a SUBSTANTIVE autopilot-row field changes (assignee /
status / execution_mode) — cosmetic edits (title/description/template) write none.
Both run inside the existing handler tx (atomic with the autopilot write).
- Dispatch resolution: both autopilot execution modes now resolve the active rule
version and stamp originator_source='rule_owner', accountable_user_id=publisher,
rule_version_id=<snapshot>, with originator_user_id left NULL (authorization
unchanged). run_only stamps CreateAutopilotTask directly; create_issue resolves in
attributionForIssueTask so both modes attribute identically. A missing version /
non-member publisher degrades to unattributed — never fabricates a human.
- finalizeAttribution now enforces the invariant ONE-WAY: it mirrors originator onto
accountable only when originator is valid, leaving an explicitly-set accountable
(rule_owner / future owner_fallback) intact when originator is NULL. Added
rule_version_id to CreateAgentTask so the create_issue path persists it too.
Also merges origin/main and renumbers this branch's attribution migrations
150/151 → 157/158 (main's #5162 took 150/151); rule_version table is 159/160.
Tests: attribution unit RuleOwner + one-way invariant table; service integration
tests proving an autopilot-origin issue stamps rule_owner + rule_version_id (and
degrades to unattributed with no version). Full service/attribution/handler/
migration suites pass on a DB migrated through 160; build/vet/gofmt clean.
Deferred (same PR): trigger-table republish (cron/webhook/event_filters) and
system-pause/archive versioning; owner_fallback + fail-closed; manual rerun.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): manual autopilot trigger → direct_human to the triggering member (MUL-4302)
Elon's blocking finding: a member manually triggering an autopilot was attributed
rule_owner (accountable = rule publisher, originator NULL) like a schedule/webhook
run, so member B triggering member A's autopilot landed accountable=A and carried
no originator authorization context for the run. Per MUL-4302 §4 a manual "run now"
is a direct human action and must attribute direct_human to the triggering member.
- Thread the triggering member from TriggerAutopilot into dispatch: new
DispatchAutopilotManual carries actorUserID (resolved via resolveActor +
memberActorUserID, so only a member actor is a human; an A2A agent actor falls
back to rule_owner). DispatchAutopilot / DispatchAutopilotForPlan keep their
public signatures (pass an invalid actor); only the internal dispatchAutopilot /
dispatchCreateIssue / dispatchRunOnly gained the param, so the many existing
callers are untouched.
- run_only: dispatchRunOnly stamps direct_human (originator == accountable ==
actor, no rule_version) for a manual actor, else rule_owner. CreateAutopilotTask
gains an originator_user_id param for the manual case.
- create_issue: dispatchCreateIssue enqueues a manual trigger via the actor-carrying
*WithHandoff entry points; attributionForIssueTask's autopilot-origin rule_owner
branch is now guarded on !actorUserID.Valid, so a valid actor falls through to the
direct_human override. Both execution modes attribute identically.
- schedule / webhook keep rule_owner (no actor). Trigger-table + system-pause/archive
versioning remain the pre-merge follow-ups.
Tests: the run_only row assertion Elon asked for (schedule → rule_owner row on
CreateAutopilotTask), plus manual direct_human on BOTH modes (run_only and
create_issue), including a manual actor distinct from the rule publisher. Full
service/attribution/handler/migration/scheduler/cmd suites pass on a DB migrated
through 160; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).
owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
accountable = agent owner, originator stays NULL (audit-only, authz untouched),
Source.Precise()==false. finalizeAttribution's one-way invariant already allows
accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
run does not start). So no run is left without an accountable human, and a
compliance workspace can block unattributable runs instead.
manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
daemon's prompt context but must attribute to whoever clicked rerun, not the
original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
the rerun path only (keeping the shared CreateAgentTask insert untouched), so
system retry (retry_of_task_id) and human rerun stay separable in reporting.
Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): close fail-open holes + move rerun_of_task_id into creation snapshot (MUL-4302)
Addresses Elon's two must-fixes on PR #5150.
1. accountable-never-null / fail-closed had fail-open holes. applyAttributionFallback
now, for an UNATTRIBUTED run, refuses the enqueue (ErrAttributionFailClosed) in
THREE cases instead of silently degrading to a runnable NULL-accountable task:
- workspace policy read fails (or no workspace) → fail closed; we cannot confirm
fallback is permitted, so we don't run an unattributable task on a DB hiccup.
(Only the rare unattributed path pays this; precise runs never read the policy.)
- workspace is fail-closed → refuse (unchanged).
- owner_fallback has no valid agent owner → refuse rather than enqueue a task with
a NULL accountable_user_id.
ErrAttributionFailClosed's doc now covers all three "cannot guarantee an
accountable human" refusals. Added missing-owner / policy-read-failure /
precise-passthrough tests.
2. manual rerun rerun_of_task_id was a post-notify UPDATE (race: the queued event /
daemon claim could see rerun_of_task_id = NULL, and a failed update degraded the
run to a plain direct_human). It now rides the CreateAgentTask insert — threaded
through enqueueIssueTask / enqueueMentionTask as a creation param (like
retry_of_task_id) so it is written in the same statement before the daemon is
notified. Removed the SetAgentTaskRerunOf follow-up query.
Also merges origin/main (unrelated CLI fix #5167, no conflict). Full service /
attribution / handler / migration / scheduler / cmd suites pass on a DB migrated
through 161; build / vet / gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): rule_owner versioning on trigger edits + system-pause/archive (MUL-4302)
The final remaining Phase 1 item: substantive publishes beyond the autopilot row now
republish the rule version, so a run's rule_owner accountable follows whoever last
changed what the rule does.
- Extracted the config-summary + insert into service.RecordAutopilotRuleVersion so
the handler and the (different-package) failure monitor share one writer; the
handler's recordAutopilotRuleVersion is now a thin wrapper.
- Trigger edits: UpdateAutopilotTrigger and DeleteAutopilotTrigger republish the rule
version with the acting member as publisher, ATOMICALLY (tx-wrapped mutation +
version write, mirroring CreateAutopilot/UpdateAutopilot). CreateAutopilotTrigger
republishes best-effort — the webhook path mints its token with a retry loop that
cannot share one tx, and a create is usually initial setup already covered by v1;
a failed write there is benign (active version stays the current publisher, the new
trigger fires under it, no immediate daemon claim rides it).
- Archive (DeleteAutopilot) republishes (member, status=archived), tx-wrapped.
- System auto-pause (failure monitor) republishes with a 'system' publisher,
best-effort — a background sweep to a non-dispatching state (a paused autopilot
never dispatches; a later member resume supersedes).
- RotateWebhookToken / SetSigningSecret deliberately do NOT version: they rotate
credentials, not the rule's behavior (not §3.4 substantive).
Semantics: a system-published (no-member) active version degrades dispatch to
unattributed → owner_fallback, never fabricating a human.
Tests: republish-reattributes (member A → member B supersedes → dispatch resolves to
B; system publisher → unattributed). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Also merges origin/main (unrelated frontend feature #5074).
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): make trigger-create rule-version republish atomic (MUL-4302)
Addresses Elon's final Phase 1 blocking finding: CreateAutopilotTrigger recorded the
rule-version republish best-effort AFTER the trigger insert. If member B added a
schedule/webhook trigger to member A's autopilot and the version write failed, future
schedule/webhook dispatches would keep attributing to A — violating the rule_owner
invariant that the last member to substantively change the rule owns future runs
("no immediate daemon claim" doesn't save it, since the miss surfaces at the LATER
trigger firing).
Both create paths now write the version in the SAME tx as the trigger INSERT:
- schedule create: wrap CreateAutopilotTrigger + recordAutopilotRuleVersion in one tx.
- webhook create: each mint-with-retry attempt runs in its own tx (insert + version
commit together; a token collision rolls that attempt back and retries with a fresh
token; a version-write failure rolls the trigger back). Passes ap + the acting
member id into the helper.
- removed the best-effort recordTriggerRuleVersionBestEffort helper (and the now-unused
slog import).
Test: TestCreateTrigger_RepublishesRuleVersionAtomically drives both create paths
through the handler and asserts a rule version is published by the acting member.
Existing webhook/trigger/archive handler tests still pass. Also merges origin/main
(unrelated avatar feature #5074). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.1 — surface run attribution on the task API (MUL-4302 §9)
First Phase 2 (visibility) increment: the agent-task API now returns the resolved
accountable-human provenance so the UI can render an "on behalf of" badge.
- AgentTaskResponse gains an `attribution` object: source label (never blank —
pre-migration NULL renders "unattributed") + `precise` flag (false for the degraded
owner_fallback / backfill / unattributed sources), the initiator (accountable) and
originator (authorization) user refs, the evidence {kind, ref_id} pointer, and the
rule_version / delegated / retry / rerun lineage ids.
- The label + evidence + raw ids are built in the PURE taskToResponse (no DB), so
every task response carries them. Names are hydrated separately, only on the
user-facing surfaces (ListAgentTasks, ListWorkspaceAgentTaskSnapshot, RerunIssue,
CancelTaskByUser) — daemon-claim paths stay lean.
- Hydration resolves initiator/originator from the GLOBAL user table (departed-member
safe) via a new batch GetUsersByIDs query (no N+1); best-effort, so a lookup hiccup
leaves the raw ids intact.
Tests: pure taskAttributionBase (direct_human / rule_owner NULL-originator /
owner_fallback degraded / pre-migration→unattributed) + DB hydration (fills known
ref, leaves unknown id un-filled, skips nil). Full handler/service/attribution/
migration/scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt
clean. The field is additive — the frontend's parseWithFallback ignores unknown keys,
so nothing breaks until the UI increment consumes it.
Also merges origin/main (unrelated editor feature #5090).
Remaining Phase 2 (next increments, same PR): frontend zod schema + "on behalf of"
badge + evidence-chain jump; append-only correction events (write + display).
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.2 — on-behalf-of badge in the execution log (MUL-4302 §9)
Surface the accountable human on every agent run row:
- AttributionBadge composes Badge + ActorAvatar, shows "on behalf of <member>"
with the resolution source as a tooltip; degraded (non-precise) attribution
gets a warning tone, and an unresolved initiator renders an explicit
"no responsible member" chip.
- Wire the badge into both active and past rows of the execution log.
- Mirror the attribution shape into AgentTaskResponseSchema (defensive, .loose())
so the cancel-task path carries it through zod; add parse tests.
- Export TaskAttribution/AttributionUser/TaskEvidence from @multica/core/types
and add the attribution block to all four issues.json locales.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): hydrate initiator names on issue-facing task endpoints + bound the badge (MUL-4302 §9)
Address Elon's PR #5150 review:
- ListTasksByIssue (the execution-log data source), GetActiveTaskForIssue and the
issue-scoped CancelTask now call hydrateTaskAttributions, so the "on behalf of
<member>" badge shows the real member name on issue detail instead of falling
back to "someone". Mirrors the existing ListAgentTasks / snapshot behavior.
- AttributionBadge: cap width (max-w-40, min-w-0) and truncate the name span so a
long name / narrow right column can't squeeze out trigger/status/actions; keep
the avatar shrink-0.
- Add a handler test asserting the issue task list returns a hydrated
attribution.initiator.name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): use semantic AvatarSize 'xs' for the badge avatar
main refactored ActorAvatar.size from a raw pixel number to the semantic
AvatarSize union (packages/ui/lib/avatar-size). Switch the on-behalf-of badge
avatar from size={14} to size="xs" (16px) after merging main.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): stage-cascade falls back to parent-issue provenance, not agent owner (MUL-4302)
When closing the last sub-issue in a Stage wakes the parent's assignee agent, the
run was enqueued via a system-authored child-done comment with no actor, which the
resolver classified as unattributed and then degraded to owner_fallback (the agent's
own owner). That is the wrong accountable human: the woken run should be accountable
to whoever caused the parent issue to exist.
attributionForIssueTask now detects a system-authored trigger comment and falls
through to the parent issue's own provenance — the same creator / agent_create-origin
/ autopilot-origin chain a direct enqueue resolves (so an agent-decomposed parent
attributes via delegation to the human who drove it; a member-created parent to that
member; an autopilot parent to the rule publisher). owner_fallback is now only the
last resort when the parent provenance itself has no human.
- Extract attributionFromComment so attributionForIssueTask can inspect author_type
without a second GetComment; authorization resolution stays byte-identical.
- Add a DB-backed test asserting a system child-done comment resolves to the parent
issue's origin human (delegation), not owner_fallback.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)
Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)
- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
FK/cascade). Capture the creating member at both trigger-create sites (schedule +
webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
carries no human authorization — same authz-safe divergence as rule_owner),
accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
dispatch and the create_issue path (bridging issue → active run → trigger_id).
Legacy triggers with no recorded creator, and agent-created triggers, degrade to
rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
tests (member creator → trigger_owner; creatorless → rule_owner fallback).
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): re-trigger CI (dropped synchronize event on
|
||
|
|
a847f6f644 |
fix(properties): address review round 3 — cache reconcile, merged-scope order, GIN-indexable filter, pool loader
- Cache reconciliation: property value writes (mutation settle + WS event) now invalidate every issue window whose server-side shape depends on property values — queries filtered by `properties` or sorted by `property:<id>` (detected via query-key predicate), covering flat lists, assignee groups, and my-issues variants. Windows without property params keep the cheap in-place patch. Fixes stale ordering/membership/counts under staleTime:Infinity. - My Issues "All" scope: merged assigned/created/involves results are re-sorted with a comparator mirroring the server ORDER BY semantics (including property sorts and missing-last, created_at DESC tiebreak) in both the flat and assignee-grouped merge paths — relation concatenation no longer overrides the user's sort. - Filter predicate rebuilt as plain bind-parameter containment ORs (AND across definitions): EXPLAIN now shows BitmapOr over idx_issue_properties_gin (the correlated jsonb_array_elements form defeated the index). Alternatives capped at 256 bind params. - Property-grouped board gains a pool loader strip: one sentinel per status that still has server rows, keeping every issue reachable until per-column pagination lands (MUL-4493). - Windowing regression test hardened: explicit positions + an assertion that the unfiltered first page excludes the target (the old fixture tied at position 0 and the created_at DESC tiebreak put the target on page one, proving nothing). - Rollback safety: /api/properties 404 (old server) degrades to an empty catalog instead of a query error, which also keeps property params from ever being sent to pre-property servers; migration 179's CHECK constraints switch to NOT VALID + VALIDATE so the exclusive lock is instantaneous. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fceb9e90df |
feat(properties): server-side property filtering and sorting on list endpoints
Property filter/sort now execute in the database, so results are correct
across the full issue set — not just the loaded 50-per-status window
(closes MUL-4493 item 1's filter/sort half; requested on MUL-4463).
- New `properties` query param on ListIssues and ListGroupedIssues:
JSON {definitionId: [values]} compiled to an AND-of-ORs containment
check (double NOT EXISTS over jsonb_array_elements). One value expands
to every storage shape it could match — string (select), array element
(multi_select), boolean (checkbox) — so the handler stays type-agnostic.
Guarded at 20 definitions / 50 values.
- `sort=property:<definitionId>` resolves the definition and orders by a
typed expression (numeric CASE cast for number, NULLIF text for
date/text/url); missing values sort last in both directions. Malformed
ids 400; unknown/archived definitions degrade to position order instead
of breaking stale clients.
- Frontend: the property filter and property sort ride the IssueSortParam
window bag, so every surface (workspace + my-issues variants), query
key, and per-status load-more page carries them automatically. The
client-side re-sort layer is gone; applyIssueFilters keeps its property
predicate as an optimistic-update backstop.
- Regression test seeds 55 issues and proves a match at position 55 is
returned by a filtered 50-row page, plus sort order/missing-last,
AND-across-definitions, and the 400/fallback sort paths.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
9e4c73f8f4 | fix(issues): stabilize scroll restoration (#5398) | ||
|
|
c10bfa8f56 |
Revert "perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#…" (#5395)
This reverts commit
|
||
|
|
ea9a470343 |
fix(properties): address MUL-4463 review round 2 — desc sort, option bucketing, archived-state reconciliation
- sort: direction now applies to value comparison only; issues without a value sort last in BOTH directions (the whole-array reverse flipped them to the front on desc). Test covers the desc+missing case. - board: values referencing an option removed from the definition bucket into the No-value column instead of vanishing (unmatched column ids dropped the issue entirely). Defense-in-depth behind the new server-side in-use guard; drag-utils test locks both behaviors. - controller: persisted propertyFilters keyed by archived/deleted definitions are stripped before reaching the filter predicates, and a persisted property sort on a non-active definition degrades to manual order — previously both kept silently applying while the header claimed otherwise. The filter badge counts only active-definition filters. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
784d9dd08a |
feat(web): custom-property list surfaces — filter, cards, sort, board grouping (MUL-4463 M2)
Brings custom properties to the issue list surfaces on top of the M1 definitions/values core: - Filter: per-definition sections in the Filter dropdown (select / multi_select options with color dots and counts; checkbox as Yes/No pseudo-options). OR within a definition, AND across definitions; client-side in applyIssueFilters, mirrored into filterAssigneeGroups for the assignee-grouped board. Included in active-filter count and Clear all. - Cards: per-property Display toggles (cardPropertyIds) render value chips on board cards and list rows via CustomPropertyValueDisplay. - Sort: SortField gains property:<id> for number/date definitions. Server keeps position order (fixed sort enum); the surface controller re-sorts client-side, swimlane/gantt reuse the same comparator. Date-only strings compare lexically; missing values sort last. - Board grouping: IssueGrouping gains property:<id> for select definitions — one column per option (definition order) plus a trailing No-value column, option-colored headings. Drag-drop moves position via UpdateIssue and applies the value through useSetIssueProperty/useUnsetIssueProperty (properties are not part of UpdateIssueRequest). Stale persisted property groupings fall back to status columns. View-store: propertyFilters + cardPropertyIds persisted via the partialize allowlist; clearFilters resets property filters; new fields deep-merge cleanly into pre-existing persisted snapshots. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
aa0946cf66 |
fix(properties): address MUL-4463 review round 1 — mobile CI, option guard, mutation safety, schema tolerance
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
645ea20b00 |
feat(web): custom properties settings tab + issue sidebar editors (MUL-4463)
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
40da795f6c |
perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#5349)
* perf(inbox): virtualize notification list (MUL-4474) The inbox notification list rendered every item at once. Each row mounts an avatar + hover card, so a long inbox inflates the tab-switch commit — the same render-amplifier class this issue targets. Extract an InboxList component that virtualizes the rows via react-virtuoso (customScrollParent over the existing overflow-y-auto element, same pattern as the issue-detail timeline). Only the visible window plus a small overscan is mounted; everything else — selection, hover, archive, scroll semantics, the row component and callbacks — is unchanged. Virtualization changes exactly one thing: whether an off-screen row is in the DOM. Slice 2a of MUL-4474 (inbox is the no-DnD surface, done first to prove the Virtuoso + scroll + keyboard harness before the drag surfaces). Draft: must pass the manual zero-functional-change pass on a real Desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(board): virtualize board columns (MUL-4474) Each board column rendered every card at once; cards carry pickers, avatars, and a per-issue activity indicator, so a tall column inflates the tab-switch commit. Virtualize the cards within each column via react-virtuoso, using the column's own scroll container as customScrollParent. The dnd-kit droppable stays on the always-mounted column scroll container (merged callback ref feeds both dnd-kit and Virtuoso), and SortableContext still wraps the full id list. So cross-column drops (status/assignee change) and reorder among on-screen cards are unchanged; reordering to an off-screen target relies on drag auto-scroll to mount it — the documented virtualization tradeoff, to be confirmed in the manual pass. The infinite-scroll sentinel rides Virtuoso's Footer slot so loadMore still fires at the bottom, and a per-item pt-2 reproduces the previous space-y-2 gap with padding inside the measured item box. issues-page.test.tsx: mock react-virtuoso to render items inline (jsdom has no layout), and make the useDroppable mock's setNodeRef referentially stable to match real dnd-kit — the board's merged customScrollParent ref would otherwise loop on a fresh ref each render. Slice 2b of MUL-4474 on the shared inbox/list/board/swimlane branch. Draft: requires the manual zero-functional-change pass on a real Desktop build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(list): virtualize issue list rows (MUL-4474) The status-grouped list rendered every row in every expanded section at once; each row carries a sortable, context menu, tooltip, and activity indicator, so a long list inflates the tab-switch commit. Virtualize each expanded section's rows with react-virtuoso, all instances sharing the page's single scroll container as customScrollParent. Everything structural is preserved by construction: the Base UI accordion, sticky status headers, collapse, the per-section useDroppable, the per-section SortableContext, and the load-more sentinel (now Virtuoso's Footer). The Virtuoso only mounts for an expanded section (a collapsed/hidden panel has no viewport to measure). Virtualization changes exactly one thing: whether an off-screen row is in the DOM. issue-surface.test.tsx: mock react-virtuoso inline (jsdom has no layout) so the surface-level loading-semantics assertions still observe the list's rows. Slice 2c of MUL-4474 on the shared branch. Draft: requires the manual zero-functional-change pass on a real Desktop build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(swimlane): virtualize lanes (MUL-4474) The swimlane rendered every lane (each a full row of status cells) at once. Virtualize the vertical lane axis with react-virtuoso over the board's outer scroll box (customScrollParent), so only on-screen lanes stay mounted. Behavior is preserved: pinned lanes keep their leading position, the SortableContext still wraps the lane set for grip-drag reorder (its items are only the non-pinned lane ids), per-cell droppables and per-cell card SortableContexts are unchanged (cells live on mounted lanes), the sticky status header stays above the list, and the per-status load-more sentinels ride Virtuoso's Footer. pt-4 per lane reproduces the previous gap-4. swimlane-view.test.tsx: mock react-virtuoso inline so the ~47 lane/cell/DnD assertions still see the lanes the virtualized list renders. Slice 2d of MUL-4474 on the shared branch — this completes the four surfaces (inbox/board/list/swimlane). Draft: requires the full manual zero-functional-change pass on a real Desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): don't pass undefined to Virtuoso `components` (MUL-4474) react-virtuoso seeds its `components` prop with an internal `{}` default; passing `components={undefined}` (which the list and board did when there was no Footer — hasMore false / no column footer) overwrites that default with undefined, so Virtuoso's startup destructure of `EmptyPlaceholder`/`Footer` throws and the surface crashes. jsdom tests mock react-virtuoso so this only surfaced on a real Desktop build (found in manual perf testing). Return a stable module-level empty object instead of undefined. Inbox (omits the prop entirely) and swimlane (always supplies a Footer) never hit this and are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3a493ab417 |
perf(issues): de-amplify per-row agent activity indicator (MUL-4474) (#5338)
Each issue row's IssueAgentActivityIndicator subscribed to the whole workspace agent-task snapshot via useQuery. Any task change swaps the snapshot array reference, so every observing row re-rendered — on a busy workspace the snapshot changes constantly, turning one task update into a full-list re-render and inflating the tab-switch commit. Narrow each row's subscription to this issue's tasks with a `select` (selectIssueTasks). React Query's structural sharing keeps the selected value referentially stable when the issue's own tasks are unchanged, so a snapshot invalidation now only re-renders the rows whose tasks actually moved. This is slice 1 of MUL-4474 (render de-amplification). Virtualization of list/board/swimlane/inbox and the non-position useSortable mount change are tracked separately — they need interactive drag + DevTools Performance verification that the headless runtime can't provide. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47e3fdedd0 |
feat(projects): start_date / due_date UI + create-project footer aligned with create-issue (MUL-4388) (#5331)
* feat(projects): add start_date / due_date pickers to project create modal and sidebar #5313 landed the backend start_date/due_date fields + types but deliberately shipped no UI. Wire up the two editor surfaces users expect: - ProjectStartDatePicker / ProjectDueDatePicker mirror the issue pickers (same calendar-day contract, clear idiom, shared @multica/core/issues/date helpers) but are typed to UpdateProjectRequest and scoped to the "projects" i18n namespace. One component serves both surfaces via a custom trigger. - Create-project modal: two date pills; values flow into the create payload and the persisted draft (draft-store gains startDate/dueDate). - Project sidebar (project-detail): two PropRows after Lead, wired to the update mutation, with clear support (send null). - i18n: prop_start_date / prop_due_date / clear_date across en/zh-Hans/ja/ko, reusing the existing issue date wording. Tests: picker display + clear behavior (real popover), and the create modal renders both pills. typecheck + lint + i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(projects): align create-project footer with create-issue Restructure the create-project modal footer to match the create-issue pattern (per design feedback): the primary action moves out of the cramped single justify-between row into its own border-t action strip, and the property pills sit in a dedicated wrapping toolbar above it. Low-frequency fields (start/due date) collapse into a ⋯ overflow via progressive disclosure — a pill only renders inline once its date is set or the user opens it from the menu — so the default toolbar stays a clean single row (Status · Priority · Lead · Repos · ⋯). - Use the shared PillButton (../common/pill-button) instead of the modal-local copy, gaining the data-popup-open styling create-issue uses. - ProjectStartDatePicker / ProjectDueDatePicker gain controlled open props so the overflow menu can reveal + open them (mirrors the issue pickers). - i18n: create_project.set_start_date / set_due_date / more_options_aria across en / zh-Hans / ja / ko, reusing the create-issue wording. Test updated to assert the dates are revealed from the overflow rather than shown inline by default. typecheck / lint / i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(views): extract shared DateOnlyPicker base for date pills (Elon nit2) The issue and project start/due-date pickers were near-complete copies of the same Popover + Calendar + clear wiring, so they could drift in behaviour or formatting. Extract that into one entity-agnostic DateOnlyPicker (packages/views/common/date-only-picker.tsx); each of the four pills is now a thin wrapper supplying only its field name (via onChange), icon, overdue flag, and localized copy. -264 lines of duplication, single source of truth. Behaviour is unchanged: the issue pickers keep their full API (trigger / triggerRender / open / onOpenChange / align / defaultOpen — all still used by board-card, issue-detail, create-issue) and the calendar-day contract stays in @multica/core/issues/date. The en-US display format now lives in one place rather than being duplicated per entity. Full views test suite (1857 tests) + typecheck + lint pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
80e54094a8 |
feat(issues): sticky setting for the issue comment bar (MUL-4435) (#5293)
* feat(issues): add sticky setting for the issue comment bar (MUL-4435) The bottom comment composer can now pin itself to the scroll viewport so it stays reachable while reading a long timeline. A pin toggle on the bar itself persists the preference (default: on) via a new localStorage-backed useCommentComposerStore. While pinned, the editor area is height-capped so long drafts scroll internally instead of covering the timeline. Co-authored-by: multica-agent <github@multica.ai> * refactor(settings): move the sticky comment bar toggle to Settings > Preferences (MUL-4435) Per review feedback: pinning the comment bar is a low-frequency choice, so the toggle moves off the bar itself into Settings > Preferences as a Switch row. The composer keeps reading the store for the sticky behavior and the 40vh editor cap; the pin button and its issues-locale tooltips are removed. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b64ebd60b5 | feat: add customizable keyboard shortcuts (#5294) | ||
|
|
d8165bac4e |
feat(views): unify reply and comment send buttons to the circular chat style (#5288)
SubmitButton is now always circular — the shape prop is removed since every composer uses the same silhouette. ReplyInput drops its inline icon-xs Button for the shared SubmitButton, gaining the same size, disabled state, and send tooltip as the comment composer and chat. MUL-4433 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c377d7fb4f |
feat(labels): add scoped label management (#5279)
* feat(labels): add scoped label management * fix(labels): address review feedback * fix(migrations): use unique label migration prefix |
||
|
|
65ccab172a |
fix(issues): float find bar above sticky resolve collapse bars (MUL-4414) (#5264)
The in-page find bar (absolute, z-20) and the resolve collapse bars pinned at the timeline's top-0 (sticky, z-20) tied on z-index, so the later-in-DOM collapse bar painted over the find bar, half-hiding it and orphaning its close button. Raise the find bar to z-30 so the transient overlay reliably paints above every sticky affordance in the content column (comment headers z-10, collapse bars z-20). Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b9b9e73e49 |
fix(issues): keep detail content column centered under classic scrollbars (#5255)
On platforms where scrollbars take layout space (macOS with a mouse or 'always show', Windows, Linux), the global 'scrollbar-width: thin' reserves ~11px on the right edge of the issue detail scroll container only, so the centered max-w-4xl column reads 32px left vs 43px right whitespace. Mirror the gutter with 'scrollbar-gutter: stable both-edges' on the detail scroller and its loading skeleton; overlay-scrollbar platforms reserve nothing and are unchanged. Measured before: 32px / 43px. After: 43px / 43px. Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
4efcfb96e3 | feat(ui): establish surface system (#5248) | ||
|
|
835b1d5e4f |
feat(issues): thread quick-jump minimap on issue detail (MUL-4389) (#5234)
* feat(issues): add thread quick-jump minimap to issue detail (MUL-4389) A Linear-style rail of tick marks overlaid on the left edge of the issue detail scroll area, one tick per comment thread (folded resolved bars included). Ticks whose thread intersects the viewport render darker, so the rail doubles as a scroll minimap. Hovering a tick grows it and opens a preview card (bold first line + muted body excerpt, both clamped); clicking jumps the timeline to the thread and flashes it like an inbox deep-link landing. Jumps go through Virtuoso's scrollToIndex in virtualized mode (the target row may be unmounted) and direct container scrollTop math in the flat deep-link/find modes, never native scrollIntoView (#3929). Viewport tracking reads DOM rects on scroll/resize instead of an IntersectionObserver because Virtuoso mounts/unmounts rows while scrolling. Hidden on mobile: no hover, and the gutter is too tight. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): Dock-style hover wave on the thread minimap (MUL-4389) Hovering the rail now magnifies ticks with a cosine falloff of their distance to the cursor — the hovered tick peaks at 1.7x and neighbours taper off across ~4 tick pitches, following the pointer continuously. Driven per-pointermove with direct style writes on the native `scale` property (compositor-friendly, no React re-render), batched read-then-write inside one rAF; a 100ms ease-out transition smooths between pointer samples and settles the collapse on leave. Clearing the inline value hands control back to the CSS floor states (popup-open, focus-visible), and prefers-reduced-motion swaps the wave for a plain hover grow. Only the hovered tick darkens — neighbours grow but keep their color. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): single glide-follow preview card on the thread minimap (MUL-4389) Scanning the rail continuously re-paid the 150ms open delay plus the close/open animation on every tick crossed, because each tick owned an independent PreviewCard popover — hover felt laggy while gliding. Replace the per-tick popovers with ONE card owned by the rail, driven by the same rAF rect pass as the hover wave: the intent delay is paid once when the pointer enters the rail; after that, gliding retargets the card instantly (~1 frame) and slides it to the hovered tick with a 150ms transform transition. Leaving starts a grace timer long enough to travel onto the card (which keeps it open for text selection); keyboard focus anchors the card immediately. The anchor is clamped so the card never sticks out of the column at the rail's extremes, and previews are cached per thread content so unrelated timeline updates don't re-flatten every comment. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bf161f2f9c |
fix(tasks): preserve merged comment delivery (#5192)
Track actual claim-time delivery, support legacy daemons, and repair comment batches across claim, retry, edit, and delete races. MUL-4348 Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
302662aee3 |
fix(issues): batch status applies directly; coalesce staged parent notifications (MUL-4155) (#5151)
Batch sub-issue status changes triggered two wrong behaviours from one user action: - Frontend popped the pre-trigger "现在开始处理?" confirm modal for every non-backlog target, but done/cancelled can never start a run, so it degenerated into a misleading "won't start → OK" step. handleBatchStatus now applies directly (product decision: batch status, including backlog → active promotion, applies like a single-issue/CLI change). Assign agent/squad and delete still confirm. The now-unreachable status mode is removed from RunConfirmModal and its locale keys. - Backend evaluated the stage barrier per-child inside the batch loop, using a mid-batch sibling snapshot. A batch closing several stages at once emitted one comment per intermediate stage, pinned the parent assignee's wake to a stale "advance Stage N+1" instruction (the accurate wake was swallowed by the pending-task dedup), and the outcome depended on issue_ids order. BatchUpdateIssues now collects terminal transitions and evaluates each parent once against the batch's final state (notifyParentsOfBatchChildDone): at most one accurate comment + one wake per parent, order-independent. Single-issue UpdateIssue is unchanged; WillEnqueueRun is untouched. Tests: cross-stage batch done/cancelled (forward + reverse) and lower-stage-only on the backend; status-direct / assign-confirm / delete-confirm routing on the frontend. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f4de0948a2 |
refactor(ui): unify ActorAvatar size tiers + round all avatars & cropper (MUL-4277, MUL-4184) (#5133)
* refactor(ui): converge ActorAvatar size to semantic tiers (MUL-4277) Replace the free-form numeric `size` on ActorAvatar with a constrained `AvatarSize` union (xs/sm/md/lg/xl/2xl) so avatar dimensions are chosen by role instead of ad-hoc pixels. This eliminates the magic-number drift where the same role rendered at different sizes across pages. - Add `@multica/ui/lib/avatar-size` (AvatarSize union + AVATAR_SIZE_PX map + default tier). - Base `ActorAvatar` (packages/ui) and business `ActorAvatar`/`AgentStatusDot` (packages/views) now take `AvatarSize`; internal font/icon math and the presence-dot threshold read px from the map. - Migrate all web/desktop call sites (packages/ui + packages/views) from numeric sizes to tiers using the role table (12,14->xs 16,18,20->sm 22,24,28->md 30,32,34->lg 40,44->xl 56,64->2xl). - Token-ise the derived consumers `AgentAvatarStack` and `IssueAgentActivityIndicator` (px looked up internally for overlap/+N math). Out of scope (per plan): ui/avatar.tsx primitive, account-tab/AvatarPicker, mobile, and component-name disambiguation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): unify all avatars and the upload cropper to round (MUL-4277, MUL-4184) main's avatar-shape decision rendered non-human actors (agent, squad, system) and the workspace logo as rounded squares, and the upload cropper mirrored that with a square crop window. Per the updated decision (avatars_and_cropper_round_required), every avatar and the crop UI are now circular; the square path is removed rather than left as dead config. - Base ActorAvatar: always rounded-full (drop the isHuman/rounded-md split). - avatar-crop-dialog: remove the AvatarCropShape/square path; crop window is always cropShape="round". - avatar-upload-control: drop VARIANT_SHAPE; the control is always round and no longer threads a shape to the dialog (variant still drives the fallback). - Strip rounded-md/rounded-none square overrides from agent/squad/member ActorAvatar call sites; round the read-only agent/squad static wrappers. - WorkspaceAvatar: round the org logo so it matches the (now round) workspace upload/crop and the shared avatar shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): make round avatar shape a hard invariant (MUL-4277) Close the two remaining square squad-avatar paths flagged in review and prevent call sites from re-squaring the avatar: - base ActorAvatar: keep `rounded-full` as the last class in cn() so a call-site `className` can no longer override the circle. - SquadHeaderAvatar: drop `className="rounded"` (was overriding the base circle into a small rounded square). - SquadsPage no-avatar fallback: route through the shared ActorAvatarBase (`isSquad size="lg"`) instead of a hand-written rounded-md tile, so the fallback matches the image path — one shape source of truth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): round the agent/squad avatar loading skeletons (MUL-4277) The avatar placeholder skeletons on the agent/squad list, detail, and profile-card loading states were still rounded squares (rounded-md/lg) from the pre-round era, so the avatar visibly popped from square to circle on load — inconsistent with the round avatars and with the member/inbox/issue skeletons that already use rounded-full. Round all five: agents-page, agent-detail-page, squads-page, squad-detail-page, squad-profile-card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f02083fec |
feat(editor): Linear-style issue identifier autolink (MUL-4241) (#5090)
* feat(editor): Linear-style issue identifier autolink (MUL-4241) Bare issue identifiers (e.g. MUL-123, TES-1) now render as navigable issue chips and can be typed/pasted into a real mention, instead of staying inert text. Covers Phase 1 (readonly render) and Phase 2 (editable editor); the Phase 3 batch resolve API is intentionally deferred. Phase 1 — readonly render autolink - Pure, markdown-aware detector `preprocessIssueIdentifiers` in @multica/ui/markdown rewrites bare identifiers to `[MUL-123](mention://issue/MUL-123)`, skipping code, existing links, URLs, and file/path tokens. Runs before linkify/file-card. - `isIssueIdentifier` distinguishes a bare identifier from a real mention UUID at render time (a UUID never matches the identifier pattern). - Chat markdown and comment/description readonly both resolve identifiers to a real issue via a workspace-scoped, exact-match TanStack Query (`issueIdentifierOptions`), rendering a chip on a hit and plain text on a miss / cross-workspace / while loading. The exact `identifier ===` filter enforces the workspace prefix, since the backend search matches by number. - Autolink is opt-in per surface; the shared editable preprocess pipeline is untouched so editable content is never rewritten with fake mentions. Phase 2 — editable editor input/paste - Async ProseMirror plugin resolves a completed identifier (boundary typed after it, or found in pasted text) and swaps it for an issue mention node, serialising to canonical `[MUL-123](mention://issue/<uuid>)`. Only genuine user edits seed candidates (programmatic setContent is gated), so opening existing content never rewrites it. Resolver injected from the setup layer; no React hooks inside the extension. Tests: detector (code/link/url/path skips, dedupe), core resolver (exact match, wrong-prefix miss, empty response, key shape), chat + readonly render (hit/miss/code/canonical), and the editable extension (type/paste/miss/ inline-code/mount-safe/incomplete-token). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): scope Phase 2 autolink to the captured candidate range (MUL-4241) Howard final-review blocker: the async resolve rescanned the whole document for every occurrence of the resolved identifier, so completing a new `MUL-1` also rewrote a pre-existing `MUL-1` the user never touched (persisted-content rewrite; violated "opening existing content is not rewritten"). Fix: capture the specific candidate range(s) a user transaction introduces — the token before the caret when typing, the tokens inside the pasted slice on paste — into plugin state, mapping each range forward on every subsequent transaction. After async resolve, replace ONLY those mapped ranges, verifying each still holds exactly that identifier with intact boundaries and no code/link mark. No document-wide scan by identifier. Also skip link-marked text at capture so an existing link label is never converted. Regression tests: (1) typing a new MUL-1 converts only the new occurrence, not a pre-existing identical one; (2) paste converts identifiers inside the paste range but leaves an identical one outside it untouched; (3) an identifier already carrying an explicit link mark is not replaced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0582db356e |
feat(issues): make cancelled a default status, not filter-gated (MUL-4290) (#5135)
MUL-4261 surfaced cancelled issues only when the status filter explicitly selected "cancelled": a separate BOARD_STATUSES (six statuses, cancelled excluded) plus a runtime showCancelled gate hid cancelled from the default list/board/swimlane. That is the wrong product model — cancelled is a lifecycle state in the same category as todo/in_progress/done/blocked and should be a first-class default column. - Remove BOARD_STATUSES. Its only purpose was to exclude cancelled, which this change reverses. PAGINATED_STATUSES is now ALL_STATUSES; the surface's default visible/hidden status derivation, the assignee-grouped board's default status set, and the swimlane column fallback all use ALL_STATUSES. - Remove the `bucketedIssues.filter(status !== "cancelled")` gate in the surface data layer. Cancelled flows through to list/board/swimlane columns, header facet counts, batch selection, and isEmpty like every other status. - hiddenStatuses derives from ALL_STATUSES, so cancelled participates in the board show/hide controls consistently (hideStatus already used ALL_STATUSES). The status filter now narrows the visible set instead of unlocking an otherwise-hidden bucket. Cancelled renders last (its canonical ALL_STATUSES position). Mobile keeps its own status mirror and is out of scope. Regression tests updated: controller now asserts cancelled is a default visible status, the filter narrows (and can hide cancelled), swimlane renders the Cancelled column by default and drops it only when the filter narrows past it, and the assignee board fetches cancelled by default. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6077f1a9b0 |
feat(issues): surface cancelled issues via status filter (MUL-4261) (#5099)
* feat(issues): surface cancelled issues via status filter (MUL-4261) Cancelled issues were never visible in the web/desktop issue surface: `PAGINATED_STATUSES`/`BOARD_STATUSES` excluded `cancelled`, so the list/ board/swimlane never fetched or rendered it, and the status filter offered a "Cancelled" checkbox that resolved to an empty list. Implement plan A (fetch-always, hide-by-default): - `PAGINATED_STATUSES` now includes `cancelled`, so it is always fetched into the byStatus cache and rebuckets correctly when an issue is cancelled (previously the card was dropped). `BOARD_STATUSES` stays the default *visible* column set. - The surface gates the flattened list on the status filter: cancelled issues are excluded from `surfaceIssues` (and therefore list/board/ swimlane columns, header facet counts, batch selection, and isEmpty) unless the filter explicitly selects "cancelled". Then a Cancelled section appears, sorted last. - `hiddenStatuses` stays board-only, so cancelled is never offered as a hideable/persistent board column. Dragging a card into the Cancelled column (visible only when filtered) sets status=cancelled through the existing generic column DnD — no new entry point or copy added. Non-goals (unchanged): mobile, member/agent archive surfaces, an always-on cancelled column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): swimlane must keep the cancelled column when filtered (MUL-4261) The swimlane derived its status columns as `BOARD_STATUSES.filter(s => visibleStatuses.includes(s))`, re-imposing canonical order by intersecting with BOARD_STATUSES. Since BOARD_STATUSES omits `cancelled`, a filter-selected Cancelled column was silently dropped even though the controller's `visibleStatuses` included it — the surface fetched and gated cancelled correctly, but swimlane never rendered it. Filter against ALL_STATUSES instead: same canonical ordering, but a selected `cancelled` column now survives. `hiddenStatuses` stays board-only, so cancelled is still never a hideable/persistent column. Regression tests: - swimlane renders a Cancelled column + its cards when cancelled is in visibleStatuses, and omits it otherwise (verified failing pre-fix); - controller asserts hiddenStatuses never contains cancelled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bcad2edc9e |
feat(issues): add Cmd+F in-page find to issue detail (MUL-4126) (#4989)
* feat(issues): add Cmd+F in-page find to issue detail Replace the stopgap "find-in-page is virtualized" toast with a real find bar (MUL-4126). Cmd/Ctrl+F opens a floating bar with keyword input, live match count, and prev/next navigation that scrolls to and highlights each match. - Opening find force-renders the comment timeline flat (reusing the existing highlightCommentId escape hatch) so off-screen comments become searchable — the root cause of the original complaint. - Matches are painted with the CSS Custom Highlight API (ranges only, no DOM mutation), so highlighting layers cleanly over React-rendered markdown and the contenteditable title/description editors. - Scroll-to-match drives container.scrollTop directly (never native scrollIntoView; #3929). Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep in-page find usable without CSS Custom Highlight API On browsers lacking the CSS Custom Highlight API, `!supported` was folded into the match-collection path, so Cmd/Ctrl+F opened the bar and swallowed native find but reported 0 matches and could not navigate — strictly worse than the native find it replaced (MUL-4126 review). Feature-guard only the paint calls now: match collection, count, active index, and scroll-to-match run regardless of support, while `CSS.highlights.set/delete` / `new Highlight` stay behind the guard. The MutationObserver re-derives ranges even when unsupported so fallback counting/navigation track live DOM churn. Adds a hook test that drives the degraded path (jsdom has no highlight API) and asserts counting + prev/next still work. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a48b6f70ef |
fix(issues): replace nested "More" action submenu with "Relations" (MUL-3972) (#4847)
The issue action menu (3-dot / right-click) nested a "More" submenu inside the already-open menu, so opening the menu surfaced yet another "More" — the first level told you nothing about what was inside. Rename that submenu to the semantically explicit "Relations" (关系 / 関係 / 관계) with a Network icon, matching the noun-labelled pattern of the sibling submenus (Status, Priority, Start date, Due date). Its contents are unchanged — create/add sub-issue and set/remove parent — and stay grouped so future relation types (blocks, duplicates, related) have a home. - Rename i18n key actions.more -> actions.relations across en/zh-Hans/ja/ko - Swap MoreHorizontal icon for Network - Update the shared menu test Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c84a939c83 |
fix(views): enable multi-file selection on all attachment upload buttons (#4962)
The FileUploadButton component already fans out per-file onSelect callbacks and every editor surface already handles N concurrent uploads (drag-drop and paste were multi-file all along), but five call sites never passed the `multiple` prop, so the OS file dialog capped picks at one file: chat composer, create-issue modal, quick-create modal, issue description, and feedback modal. Fixes MUL-4074. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c4b116ec3a |
feat(views): expose add-label entry in create-issue dialog (#4846)
Adjust the manual create-issue dialog toolbar so the add-label entry is surfaced directly, and move the lower-frequency due date into the ⋯ menu: - Add a Labels picker in the slot Due date used to occupy. LabelPicker gains a draft mode (no issueId): selection is held via selectedIds / onSelectedIdsChange and attached to the issue right after it's created (the create endpoint takes no labels), mirroring the sub-issue linking pattern already in this dialog. - Collapse Due date into the ⋯ overflow menu with the same reveal rule as Start date (inline only when it has a value or was just opened). Give DueDatePicker the controlled open/onOpenChange props StartDatePicker already had. - Persist chosen labels in the issue draft store (labelIds) like every other draft field. - Add useAttachLabelToIssue (variables-based attach) so labels can be attached to a just-created issue. - i18n: add create_issue.set_due_date and toast_link_labels_failed across en / zh-Hans / ja / ko. MUL-3971 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ade6b34e5f |
MUL-3903: Extract shared issue surfaces (#4774)
* MUL-3903 refactor project issue surface state Co-authored-by: multica-agent <github@multica.ai> * Refactor project issue surface ownership Co-authored-by: multica-agent <github@multica.ai> * Extract shared issue surface entrypoints Co-authored-by: multica-agent <github@multica.ai> * Fix issue surface create defaults and selection reset Co-authored-by: multica-agent <github@multica.ai> * test(editor): add missing AbortSignal to suggestion items() calls The suggestion items() contract gained a required signal param; the mention/slash test call sites were never updated, breaking pnpm typecheck for @multica/views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): server-side assignee_types filter on ListIssues ListGroupedIssues has taken assignee_types since squads shipped, but ListIssues never did — so the workspace Members/Agents tabs had to fetch the unfiltered workspace list and post-filter loaded pages client-side, which made column totals and load-more pagination reflect the unfiltered counts. Add the same parse + WHERE clause to ListIssues (count query shares the WHERE, so totals agree), thread the param through the TS client, and widen MyIssuesFilter so scoped list caches can carry it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(issues): route issue cache writes through a membership-aware coordinator useUpdateIssue, useBatchUpdateIssues, and the WS issue:updated handler each maintained their own similar-but-diverging patch/invalidate rules. Consolidate them into cache-coordinator.ts (applyIssueChange / rollbackIssueChange / invalidateIssueDerivatives) so local writes and remote echoes follow one rules table by construction. The coordinator is membership-aware via surface/membership.ts (true | false | unknown against each list cache's own filter contract): - a change that moves an issue off a filtered surface removes the card surgically (bucket total decremented) — fixes assignee changes leaving stale cards on My Assigned with no local safety net (previously only the WS echo recovered it), and replaces the blanket invalidate-myAll net for project moves (MUL-3669) with per-key precision - possible entry into a loaded list marks that key stale — never hard-insert; page/slot is server knowledge - stale keys flush on settle for mutations (a mid-flight refetch would stomp the optimistic state) and immediately for WS - batch updates now patch detail + inbox like single updates; the off-screen bucket-count recovery previously exclusive to the WS path now covers local mutations too Preserved invariants: synchronous optimistic patches (dnd-kit), MUL-3375 control-field stripping, and no refetch of surgically reconciled lists (the drag-flicker fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(issues): resolve surfaces via core query plan/repository with window-keyed remount Read-path convergence and the loading/empty semantics that fall out of it: - scope -> API params moves from scope.ts helpers into surface/query-plan.ts; workspace members/agents become server-filtered scoped plans (assignee_types) and the client postFilter machinery is deleted — tab counts and load-more are now exact - query selection moves behind surface/repository.ts; the views data hook no longer branches on workspace-vs-scoped plumbing - IssueSurfaceContent remounts on data-window change (wsId + scope): keepPreviousData placeholders keep sort/filter changes flicker-free within one window but must never let project A's (or workspace A's) cards impersonate B's with no loading state — cold window shows the skeleton, warm window hits cache instantly - isEmpty is only asserted from full-window data; the gantt scheduled-only projection can't prove the window is empty, so GanttView's own "no scheduled issues" empty state renders instead of the generic create-issue one - per-card project lookups hoist into a surface-level projectMap (drops a per-card useQuery), create-defaults typing tightens to IssueCreateDefaults Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(issues): count-only arithmetic for off-window status/membership changes An issue beyond a list's loaded page window used to force a full first-page refetch just to fix two column counts. When the change is CERTAIN (base entity known, membership definitive) the coordinator now does the arithmetic locally: - stayed a member + status changed: move one unit of total between the two buckets (loaded arrays untouched; hasMore stays consistent) - left the list (reassigned / re-projected): old status bucket total -1 - member-to-member reassignment: counts unaffected, not even a stale key Entering a list and any uncertainty (no base, unknown membership) still refetch — the right page/slot is server knowledge. Branches on membership OUTCOMES, not on which field changed, so future dimensions (team) join automatically. Biggest win is the WS path: agents flipping off-screen statuses no longer trigger refetch storms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): deferred view-refresh indicator during placeholder revalidation Sort/date changes (and any grouped-board filter change) revalidate behind the previous snapshot — correct, but on a slow network the click felt dead: content stays put and isLoading never fires. Surface the state as isRefreshing (isPlaceholderData of the active query) and render a shared ViewRefreshIndicator in every issues header: a fixed-width slot (zero layout shift) whose spinner fades in after 300ms, so sub-second responses show nothing (NN/g) while slow ones get a working signal. Bound to the revalidation STATE, not to any particular control — any current or future server-side view change lights it automatically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5ed381a9d6 |
Fix comment attachment URL resolution (#4816)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8c3745dc7e |
feat(issues): add 'Show sub-issues' display toggle (MUL-3923) (#4801)
* feat(issues): add 'Show sub-issues' display toggle (MUL-3923) Add a 'Show sub-issues' switch to the issue display-settings menu. When turned off it hides sub-issues (issues with a parent) from the board, list, swimlane and gantt views so users can focus on top-level / parent issues. It is a pure display filter and never changes parent/child relationships. Defaults to on and persists per view store, so /issues, /my-issues, project detail and the actor tasks panel each remember it. - view-store: new showSubIssues state + toggleShowSubIssues action, persisted; propagates to my-issues and actor view stores via the shared slice. - filter: optional showSubIssues on IssueFilters; drop issues with a parent when explicitly false (undefined keeps show-all, so existing callers and mobile's positional variant are unaffected). - wire the toggle into every surface that renders the display menu. - i18n for en / zh-Hans / ja / ko. - filter unit tests for the new toggle. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): apply Show sub-issues to assignee board & swimlane extra path (MUL-3923) Address review of #4801 — two paths bypassed the new toggle: 1. Assignee-grouped board rendered straight from the server 'groups' array, skipping the flat filterIssues() output, so sub-issues stayed visible in assignee grouping on /issues, /my-issues and project detail. Added filterAssigneeGroups() which re-applies the client-only display filters (Show sub-issues + the agents-working quick filter) to each group, recomputes total and drops emptied groups. Wired into all three surfaces. Generalizes and replaces the old filterRunningAssigneeGroups. 2. Parent swimlane's batch/per-parent extra-children merge rebuilt its internal activeFilters without showSubIssues, so lazily-loaded sub-issues reappeared even with the toggle off. Carry showSubIssues through. Tests: filterAssigneeGroups unit tests (sub-issue hide, running filter, AND composition, empty-group drop, by-reference passthrough) and a swimlane test covering the batch-children path with the toggle off. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a27f828278 | fix(issues): make comment highlight background-only (#4789) | ||
|
|
c4209ec7c0 |
fix(issues): count active issues, not agents, in working chip (#4750)
The Issues board header 'x working' chip derived its count from the set of distinct running agent_ids, so two agents on the same issue read as '2 working'. Count distinct issue_ids instead so the number reflects how many issues agents are working on — matching the filter the chip toggles, which already narrows the list to those issues. The avatar stack still shows the distinct agents behind that work. Adds workspace-agent-working-chip.test.tsx covering the multi-agent / single-issue case, multi-issue counting, scopedIssueIds filtering, and the empty state. Fixes MUL-3875 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6e2d2c003c |
fix(issues): sync sticky comment header background with highlight fade (MUL-3759) (#4690)
The deep-link highlight tint faded out over 700ms on the comment body layers but the sticky header's background switched instantly, and its 4px bottom `after` gradient band recolored by class-switching that `transition-colors` cannot animate. Both desynced from the body during the fade, showing a white header and a pale seam under it. Add `transition-colors duration-700` to the sticky shell so the header background fades with the body, and make the `after` band derive its color from the header via `bg-[inherit]` + a `mask-image` fade instead of a per-state gradient color, so all three layers are driven by the single header background transition. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
37d9fafda6 |
feat(issues): add Remove parent issue action (MUL-3764) (#4630)
* feat(issues): add Remove parent issue action to promote a sub-issue to standalone (MUL-3764) Surfaces a discoverable UI affordance for clearing an issue's parent — the backend and CLI (multica issue update --parent "") already support it, but the Official App only exposed Set parent. Adds: - A 'Remove parent issue' item in the issue actions menu (dropdown + right-click), shown only when the issue has a parent. - A hover unlink button on the parent card in the issue detail sidebar. - A removeParent handler that clears parent_issue_id and stage in one write (stage only orders sub-issues under a parent) with a success toast. Closes #4629 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): toast remove-parent on success only, prune old parent's children cache (MUL-3764) Addresses review feedback on #4630: - use-issue-actions.ts: the remove-parent success toast fired eagerly after mutate(), so a request that failed on permission/network/validation would flash "removed" before the error toast and optimistic rollback. Move it to onSuccess so only a server-confirmed detach is announced. - mutations.ts: when a write re-parents an issue away from its current parent, prune it from the old parent's children cache instead of patching it to parent_issue_id: null in place. The parent's sub-issues list renders that array directly, so the orphaned row used to linger until the settle refetch. onError still restores prevChildren, so the prune rolls back on failure. Adds cache-prune coverage (optimistic remove / rollback / non-reparenting no-op) and onSuccess-vs-onError toast coverage. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f1e6c18e3e | fix(issues): add loading state to edit comment save button (MUL-3709) (#4588) | ||
|
|
a7908e6967 |
fix(issues): sync header agent chip with execution log via shared query (#4498)
The header live chip derived its active-task state from the workspace-wide agent-task-snapshot, while the right-panel Execution log read the per-issue task list. Two queries, two endpoints, two independent refetches: the heavier workspace snapshot lands later than the per-issue list, so the log could show a running task while the header chip had not started yet. Point the chip at the same `issueKeys.tasks(issueId)` cache the Execution log uses (identical query options). Both surfaces now observe one cache entry and update atomically. Drop the now-redundant workspace-id lookup and client-side issue_id filter, since the endpoint is already issue-scoped. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3ce97453b3 |
fix(issues): pre-trigger preview + run-confirm + handoff UX polish (MUL-3375) (#4454)
* fix(issues): stop issue-trigger preview flicker The pre-trigger preview re-rendered/refetched on every workspace task event: WS task lifecycle invalidated issueTriggerPreviewAll (staleTime 0), forcing a background refetch whose isFetching was surfaced as isLoading, collapsing and reopening CreateRunHint's reveal band. The assign source (create / assignee change) cancels existing tasks before enqueuing, so its verdict can't shift from a task event at all; the status source's pending dedup could, but the preview is advisory and the write path re-evaluates authoritatively, so a rare stale label is harmless. Drop the WS invalidation so the preview refetches only on input (signature) change. Keep the comment-trigger invalidation — its verdict genuinely changes mid-compose and its chips drive an immediate, unconfirmed send. Align the hook's data handling with the comment-trigger preview: keepPreviousData so an input switch swaps in place instead of collapsing, and treat only the first load (no prior data) as loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm modal for backlog assign Assigning a Backlog issue to an agent/squad never starts a run (the parking lot — server/internal/service/issue_trigger.go), so the pre-trigger confirm modal only rendered an empty "won't start" box with a single Apply button. Apply directly instead: the single path checks issue.status, the batch path skips only when every selected issue is Backlog (mixed selections still confirm — the non-backlog ones trigger). Mirrors the existing backlog short-circuit in handleBatchStatus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(modals): run-confirm loading state + submit spinner The dialog grew in height after open: it rendered the short "won't start" variant while POST /api/issues/preview-trigger was in flight, then the note box appeared when the predicate landed. Keep the note box mounted (disabled) during loading so assign mode opens at its resolved height, and show a Spinner + 'checking' headline while loading. Submit had no feedback — buttons only disabled, which read as frozen for note assigns (the request starts an agent server-side). Track which footer action is in flight and show a Spinner on the clicked button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): show handoff note in execution-log trigger text An assignment-triggered run that carried a handoff note showed the generic "Initial run" label. Surface the note inline (truncated, like comment triggers show their text) so the row reads as the handoff. taskToResponse now populates handoff_note for all callers (dropping the now-redundant explicit set in ClaimTaskByRuntime); the field is added to the AgentTask type + zod schema (optional, additive — old clients ignore it via the loose schema, new clients fall back to "Initial run"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ab335b8a5 |
MUL-3416: Issue pre-trigger preview + Handoff Note (#4383)
* feat(issues): unify run-enqueue decision behind WillEnqueueRun + preview endpoint Collapse the issue update/batch enqueue copies into one service predicate service.IssueService.WillEnqueueRun, shared verbatim with a new dry-run endpoint POST /api/issues/preview-trigger so the four entry points stop drifting (squad/self-loop/batch omissions, MUL-3375). The private-agent gate stays at the HTTP boundary: write paths inject allow-all, preview injects the real gate so it never leaks a private agent's readiness. Add suppress_run to issue update/batch: the change applies but no run starts. Remove the now-dead handler mirrors shouldEnqueueSquadLeaderOnAssign / isSquadLeaderReady. service.Create and the comment trigger chain are untouched. Tests: preview behavior, preview<->write-path match, batch aggregation, member no-trigger, suppress_run skip, malformed-body 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): inject handoff note into assigned runs via first-class task field Add an optional handoff_note carried by issue assign/promote into the run's opening prompt and issue_context.md, via a dedicated agent_task_queue column (migration 122) and a daemon assignment-handoff render branch — never a fabricated comment, never trigger_comment_id (MUL-3375 §6.1). Thread the note through enqueueIssueTask/enqueueMentionTask + WithHandoff public variants and dispatchIssueRun; suppress_run or a parked write drops it (no run = nothing to inject). Soft version gate: MinHandoffCLIVersion + HandoffSupported, surfaced per-trigger as handoff_supported in the preview so the UI can gray the note box on old daemons; the assignment never hard-fails. Tests: daemon prompt + issue_context render via the assignment branch (not quick-create/comment), version helper matrix, note persists on the task, suppressed assign enqueues nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): leave a display-only handoff record on the timeline When an assign/promote with a handoff note starts a run, write one type='handoff' timeline record via TaskService.RecordHandoff — a direct Queries.CreateComment + timeline event that bypasses Handler.CreateComment, so it never reaches triggerTasksForComment and cannot start a second run (MUL-3375 §6.2, the must-not-retrigger invariant). Author is the actor who handed off; body is the note. Migration 123 admits the 'handoff' comment type. Recorded only on a real run start: suppress_run or a parked write writes nothing. enqueueSquadLeaderTask now reports whether it enqueued so the trace is gated on an actual dispatch. Test: exactly one handoff record on assign-with-note, exactly one task (no re-trigger), and no record when suppressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): frontend plumbing for issue-trigger preview + handoff (core) Add api.previewIssueTrigger + IssueTriggerPreviewSchema (zod parseWithFallback), the use-issue-trigger-preview hook, issueKeys.issueTriggerPreview(+All) with WS queue-state invalidation, suppress_run/handoff_note on UpdateIssueRequest, the 'handoff' CommentType, and stripping of the control fields from optimistic update/batch cache patches (MUL-3375 §9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): exclude handoff records from new-comment counting type='handoff' is a display-only timeline record, not conversation. Exclude it from CountNewCommentsSince so a handoff note never inflates the count of "new comments to catch up on" fed to a claiming agent (MUL-3375 §12). Analytics already excludes it (RecordHandoff is a direct write that emits no analytics event), and the comment-trigger path is already bypassed. Test: a handoff record does not bump the new-comment count; a real comment does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): pre-trigger preview UI, handoff note, timeline card (web/desktop) Wire the §9 frontend onto the preview endpoint + handoff fields: - Delete the backlog blocking dialog (backlog-agent-hint*) and its modal type; the over-eager nag is gone. Backlog awareness is now a passive label. - RunConfirmModal: single assign + batch assign/status route here. Shows the backend predicate's verdict ("将启动 @X" / "将启动 N 个" / parked), an optional handoff note (assign only, soft-gated by handoff_supported), and 暂不启动 — then applies via update/batch. No frontend guessing. - create modal: passive CreateRunHint ("将启动 @X" / backlog parked). - single status change stays a direct apply (unchanged). - timeline: render type='handoff' as a distinct, non-interactive handoff card. - i18n run_confirm + handoff_card across en/ja/ko/zh-Hans; drop backlog action keys; locale parity green. Tests: use-issue-actions (assign → run-confirm modal, member → direct), create-issue + comment-card suites updated/green; views typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): use a valid anchor in the handoff count-exclusion test CountNewCommentsSince filters id <> @anchor_id; SQL id <> NULL is NULL and excludes every row, so an empty anchor made the control assertion read 0. The production caller always passes a real anchor — mirror that with a non-matching sentinel uuid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): RunConfirmModal apply logic (start/suppress/note-gate/batch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(core): preview schema malformed/missing/null fallback coverage Cover IssueTriggerPreviewSchema via parseWithFallback (MUL-3375): well-formed parse, top-level + item default fills (empty/older backend), and fallback to { triggers: [], total_count: 0 } for malformed shapes, a dropped required issue_id, a wrong-typed total_count, and null/non-object bodies — so the four entry points degrade to "nothing will start" instead of throwing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): remove display-only handoff timeline record (留痕) The handoff "留痕" timeline record (type='handoff' comment written on run start) was judged superfluous and dropped per product call. This removes only the display-only trace; the handoff NOTE injection into the run's opening prompt + issue_context.md is untouched. - backend: drop RecordHandoff + its call in dispatchIssueRun - db: drop the `type <> 'handoff'` exclusion in CountNewCommentsSince and migration 123 (comment_type_check reverts to the 4-type set from 001); no production data exists for this unreleased feature - frontend: drop the "handoff" CommentType, HandoffCard, and handoff_card i18n (all locales) - tests: drop handoff_count_test.go and the record-write assertions in issue_trigger_preview_test.go (note-injection tests retained) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): dismissable run-confirm modal + team-handoff copy Two fixes to the pre-trigger confirm modal (MUL-3375). 1. Dismissable: switch RunConfirmModal from AlertDialog to the standard shadcn Dialog so it has the close (X) button + Esc + click-outside. Previously the only choices were "start" / "don't start now" with no way to abort the action entirely; dismissing now cancels with no write. 2. Copy: rework the action-surface wording away from the backend term "run" toward team-handoff voice — 指派 / 开始 / 交接 (run stays only on record surfaces). Unifies the note's three names to "交接说明", and parallels the rewrite across en/ja/ko. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(agent): bump handoff note min CLI version to 0.3.28 The daemon release that renders handoff notes ships in 0.3.28 (0.3.27 was the prior tag), so move the soft-gate threshold up. Below this the note is silently dropped and the frontend grays the note box — assignment is never blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm when batch-moving issues to backlog A move into backlog never starts a run (service/issue_trigger.go), so the pre-trigger confirm modal degenerated to an empty "won't start" box with a single Apply button — pure friction. Apply directly instead, matching the single-issue status path. Other target statuses still route through the modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): refine pre-trigger preview hint and copy - Move the create-issue run hint to a reveal band (grid 0fr→1fr) above the property toolbar. It was sharing the footer button row and, lacking a width constraint, reflowed the submit buttons whenever it appeared. Restyle to a borderless, comment-style avatar+caption that is purely a caption (non-interactive avatar). - Distinguish squad from agent in the pre-trigger copy: a squad's leader evaluates and delegates rather than "starting work" itself. Add will_start_named_squad / will_start_squad / create_will_start_squad across en/zh/ja/ko (reusing the squad_leader_* evaluate→arrange vocabulary) and branch run-confirm + the create hint on squad assignees. - Bold the assignee name in the run-confirm headline via a language-safe sentinel split (no per-language prefix/suffix keys). - Align zh "开始处理" → "开始工作" on the single-assign copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(issues): stub ActorAvatar in create-issue suite CreateRunHint now renders an ActorAvatar for agent/squad assignees, which pulls in getActorInitials/getActorAvatarUrl + the workspace/presence/navigation hook tree. This form-focused suite only stubbed getActorName, so the squad-forwarding test crashed with "getActorInitials is not a function". Stub the avatar inert — its own behavior is covered elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4679217586 |
feat(cli): STR-208 오토파일럿 구독자 플래그 추가 (#4438)
* feat(cli): STR-208 오토파일럿 구독자 플래그 추가 * test(core): Issue fixture stage 기본값 추가 * test(views): Issue fixture stage 기본값 추가 |
||
|
|
45dae3185f |
fix(issues): eliminate optimistic-update drag flicker (board, list, batch, WS) (#4415)
* fix(issues): stop kanban card snapping back on drag A cross-column drag on a non-position-sorted board left the card in its origin column for the whole request, then jumped to the target only when the mutation settled — the "snaps back, then moves" glitch. Root cause was three coupled choices in the optimistic path: - board-view never updated local columns on drop for sortBy != "position" (onDragOver is a no-op there), so the card relied on the settle refetch to move across. - useUpdateIssue invalidated the whole list on settle, replacing the column and re-landing the card even on success. - patchIssueInBuckets appended a moved card to the column tail instead of its position slot, so any later cache refresh teleported it to the end. Fixes: - board-view: optimistically move the card into the target column on drop for the non-position path (insertIdByPosition), and reconcile local columns from the cache on settle for both paths (revert on error now that the list is no longer refetched). - mutations: reconcile via onSuccess surgical patch of the returned entity; drop the list/detail invalidation from onSettled (aggregates still flush). - cache-helpers: patchIssueInBuckets inserts the moved/reordered card at its position slot; a plain field update still keeps its slot. Adds cache-helpers and drag-utils unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): patch My-Issues / Project board caches on move too The drag fix made the board reconcile local columns from its feeding cache on settle. The workspace board rides issueKeys.list (patched by onMutate), but the My-Issues and Project boards ride the myList cache, which the mutation did not patch — so a successful move snapped back on those boards. useUpdateIssue now patches/snapshots/rolls back every bucketed list cache (workspace list + myList), selected by the ListIssuesCache `byStatus` shape so grouped (assignee) and flat (gantt) caches are skipped. Adds renderHook regression tests covering both-cache optimistic move, both-cache rollback, and no-list-invalidation-on-settle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): drop redundant WS position->list invalidate onIssueUpdated already surgically patches the non-filtered workspace board via patchIssueInBuckets (cross-status move + same-column reorder). The extra `if (position) invalidateQueries(list)` re-pulled the whole board on top of that, re-introducing drag flicker through the echoed-back WS event. Removed. Filtered myAll lists still invalidate (membership can change there) — the client-side membership reconciliation for those is a separate follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): batch update patches myList + stops list refetch on settle - onMutate now patches both issueKeys.list and the filtered issueKeys.myAll bucketed caches, so a batch edit on a My-Issues / Project board is optimistic too. Previously only the workspace board was patched, so batch edits on those boards relied entirely on the settle refetch. - onSettled no longer invalidates issueKeys.list: the optimistic patch is a complete reconcile for these bucketed boards (batch changes status / priority / project, never a server-computed value), so a full-board refetch only re-introduced the flicker the single-issue path removed. Aggregate / grouped caches still refresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): list view optimistically moves row on non-position drag The sortBy != "position" branch called onMoveIssue without moving the row in local columns, so the row sat in its origin group for the whole request and only jumped across on settle -- the same snap-back the board view had before its fix. Now mirrors board-view: setColumns(insertIdByPosition) on drop so the settle rebuild is a visual no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep My-Issues/Project boards in place on non-membership WS change onIssueUpdated now surgically patches the filtered myList (myAll) caches and only invalidates them when the change can actually move an issue in/out of the filter: an assignee change (covers My-Issues direct-assignee + the involves leg + actor panels) or a project change (Project board). A pure status / position / priority / label change reconciles in place -- no refetch -- removing the last drag flicker on filtered boards. Uses the assignee_changed flag the server already sends on issue:updated (surfaced on IssueUpdatedPayload + forwarded by the realtime dispatch); project change is diffed client-side against the cached value. No predicate replication, no backend change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): add settle-lock to swimlane drag (no clobber mid-flight) The swimlane drag had no settle window: the resync useEffect (and the issueMap freeze) guarded only isDraggingRef, so a cache change landing after drop but before the move settled could rebuild localCells out from under the optimistic move. Adds isSettlingRef + settleVersion (mirroring board-view / list-view): the lock is held from drop until onMoveIssue settles, then released, forcing a single resync from the reconciled cache. onMoveIssue now accepts the same optional onSettled callback board/list already use; the parent handleMoveIssue supplies it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): extract shared useDragSettle hook for board + list board-view and list-view carried byte-identical drag/settle scaffolding (the local columns mirror, the dragging/settling locks, the post-move animation-frame throttle, and the settle callback). That duplication is exactly what let list-view silently drift earlier (it had lost the optimistic-move half of the fix, and its position-branch settle callback omitted the settleVersion bump). Extract the primitive into useDragSettle so both surfaces share one implementation and can't drift again. Behavior-preserving for board-view. For list-view the one intended alignment: its position-branch failed move now reverts, gaining the settleVersion bump board-view already had. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |