mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 17:40:11 +02:00
fix/codex-patch-apply-payload
113 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7803a5b9ea |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6136)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 written as arbitrary values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the ~1,900-call-site rename moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: 8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30 (text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small buttons and toggles. Half-pixel sizes are gone. This supersedes #6108, which was reverted by #6116 because the sidebar group labels rendered at the inherited 16px. The cause was not the scale but cn(): `text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a table listing only the default sizes, so it filed every role step under text-colour and dropped whichever of `text-caption` / `text-sidebar-foreground/70` came first. Registering the steps as a font-size class group restores the real conflict groups — size beats size, colour beats colour, the two coexist — and a test pins the list against the scale, since the failure is silent in source. Hand-written CSS is covered too. The transcript kept a 12.5px body long after every Tailwind call site was on the scale, so the "no half-pixel sizes" claim was true of the classes and false of the product; the editor's prose, code and mermaid ramps had the same blind spot, and seven of their eight values already equalled a step exactly. All now reference var(--text-*). The guard test reads raw `font-size:` declarations as well as class names, exempting only the 16px iOS input-zoom workaround in base.css and the landing pages' marketing ramp. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing display type (rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative emoji / serif-hero sizes. Verified on a running local stack: pinned sidebar rows and group labels measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit of every rendered font size across the product surfaces finds nothing off the scale; the only exceptions are avatar initials and emoji, which actor-avatar.tsx sizes proportionally to the avatar diameter by design. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
545afea827 |
Revert "feat(ui): establish a role-named type scale and migrate ad-hoc font s…" (#6116)
This reverts commit
|
||
|
|
d68d636c91 |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6108)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 of them written as arbitrary text-[Npx] values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the 1,900-call-site rename (text-sm -> text-body and friends) moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: - 8 / 9 / 10px -> micro (11px): 102 sites, mostly badges and overline labels. Deliberate — under 12px is a counter role, not a text role. - 17px -> title (18px), 22px -> display-sm (24px), 30px (text-3xl) -> display (36px): 21 sites, all headings or stat numbers in flexible containers. - Half-pixel steps are gone entirely. Arbitrary sizes also inherited whatever line-height was above them; the tokens now pin one, which removes latent overflow risk in the fixed-height h-4/h-5 badges those sizes were used in. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing-page display type (rem/clamp, 2.2-6.4rem) is marketing typography on a separate ramp and stays out of the scale, as do four decorative emoji / serif-hero sizes. A guard test fails the build on any font size written outside the scale, reporting file:line — without it nothing in a Tailwind build makes an off-scale value look wrong, which is how the drift happened. Verified against a local stack: typecheck, lint and the full TS suite show no new failures, and before/after screenshots of issues, issue detail, runtimes (populated), usage, agents, inbox, my-issues and settings differ only in the intended 10px -> 11px labels, with no row-height, truncation or layout shift. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6c9a59cc14 |
refactor(uploads): widen handleUpload contract, fix interrupted doc drift (#6035)
Follow-up to #6025 (MUL-5391), addressing the two non-blocking cleanups raised in review. 1. `CoordinatedUploads.handleUpload` was still typed as a one-arg function while the real `ContentEditor` contract is `(file, uploadId)`. Runtime was already safe (the implementation accepts `uploadId?`), but the exported interface erased the second parameter at the boundary, so a mock or hand-rolled caller could silently drop the editor-minted id and mint a second one — breaking the one-id link between the document node and the draft record. Widened the type and documented why the id must be threaded through. 2. #6025 changed `normalizeStoredUploads` to DROP persisted `uploading` records instead of coercing them to `interrupted`, but eleven comments across core and views still described the old coercion. Corrected them to state what the code does. `interrupted` is now produced by no code path at all; it stays in the union and is still accepted, rendered and dismissable because builds before this change persisted such records. Marked it LEGACY at the type and in the test that pins the behaviour. No runtime behaviour change: comments, one type widening, one test comment. Verified: pnpm typecheck (6/6); packages/core Vitest 1133 tests; packages/views Vitest 3178 tests; git diff --check. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
18adb20c14 |
MUL-5391: unify upload placeholder UI (#6025)
* fix(editor): an in-flight upload placeholder is never content, and is drawn once
Two defects with one cause: a placeholder for an upload in progress was both
serialised into the draft body and drawn a second time as a chip.
The document IS the persisted draft (getMarkdown -> setDraft), so serialising
an in-flight node turns it into text that outlives the upload:
- fileCard emitted `!file[x.pdf]()`. Its own tokenizer cannot parse an empty
href back, so the line survived reopen as dead literal text, sat next to
the real link the write-back appended, and shipped with the comment.
- image emitted its process-local `blob:` URL, which ContentEditor then
scrubbed back out with a regex on every serialise.
Both renderMarkdown implementations now emit nothing while `attrs.uploading`
is set (or no URL exists). A node becomes content the moment it holds a real
URL and never before, which is strictly stronger than scrubbing after the
fact — so BLOB_IMAGE_RE / stripBlobUrls are deleted rather than extended.
Separately, ComposerUploadChips rendered every non-`uploaded` entry, including
ones whose placeholder node is right there in the editor. Every upload started
from a live mount inserts a node first (uploadAndInsertFile is the uploader's
only caller), so those chips were the same upload drawn twice, in two visual
languages, shifting layout as they appeared and vanished. useCoordinatedUploads
now exposes `orphanUploads` — the entries inherited from the persisted draft,
whose originating mount is gone and whose node died with it. That is the case
the chip strip was introduced for, and now the only one it covers.
`getMarkdown()` deliberately stays untrimmed (see its safety-net test); only
its stripBlobUrls wrapper is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(editor): keep a failed upload visible after the chip/node split
Self-review catch on the previous commit: suppressing the chip for every
upload this mount started also suppressed it for FAILED ones. The document
cannot stand in for those — uploadAndInsertFile removes the placeholder node
on failure — so the outcome was left to a toast that has already gone.
The rule is not "started here" but "the document is showing it", and the
document only ever shows a live placeholder: still `uploading` AND started by
this mount. `failed` / `interrupted` always get a chip, `uploaded` never does
(the editor and AttachmentList render those), which also makes an
`orphanUploads.length` gate mean what the call sites assume.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(editor): keep the chip when the user deletes a running upload's placeholder
Code-review catch: "started by this mount" is necessary but not sufficient for
"the document is showing it". Cmd+Z right after a paste removes the placeholder
node while the upload keeps running — and gate.isBlocked keeps blocking send on
the store entry regardless of the node — so the previous filter left a dead send
button with nothing on screen explaining it.
The filter now also consults editorGate.uploading, which is the document's own
answer to "am I showing a placeholder right now" (sourced from the uploading-node
scan via onUploadingChange). Started-here AND still shown is what suppresses a
chip; either half failing brings it back.
Also drops a stale stripBlobUrls reference from the use-upload-gate docstring —
that helper no longer exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(editor): a failed upload leaves nothing behind
The failure chip carried no information the toast had not already given at the
moment it happened, and it could not act on it: the bytes were never persisted,
so there is nothing to retry, and the file is still on disk to re-attach. Its
only affordance was a dismiss ✕.
It cost more than that. The entry lives in the persisted draft, so it survived
reload and reopen until dismissed by hand — and `isMeaningful` counts uploads,
so a single flaky request kept an otherwise-empty draft alive for the full
30-day TTL. Uploading again did not clear it either: a new upload is a new
clientUploadId.
Failures now remove their placeholder outright instead of marking it. Both
failure paths (size check, coordinator settle) collapse into that one rule,
which also folds the paste-as-file recovery into the shared branch rather than
duplicating it.
`interrupted` keeps its chip: it is discovered a session later, when the user
no longer remembers attaching anything. `orphanUploads` still handles `failed`
because an older client may have persisted one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(editor): one upload, one node, from start to finish
The chip strip existed because the document could not answer for an upload it
was not showing. Give it that ability and the strip has no reason to exist.
Three changes make one model:
- ONE IDENTITY. The node's `uploadId` and the draft's `clientUploadId` were
two independently minted random values, because the node is inserted before
the handler that created the draft record runs. `uploadAndInsertFile` now
mints the id up front and hands it to the uploader, which adopts it. Asking
"is this upload in the document" becomes a lookup instead of an inference.
- REBUILD ON MOUNT. A placeholder is never serialised (it is not content), so
it dies with the document that drew it and a reopened composer showed no
trace of an upload still running. The draft record is enough to draw it
again. Once per id per mount: a placeholder the user deleted mid-upload
stays deleted (MUL-5181), and the guard is what stops the next store write
from undoing that. Skipped entirely while chat pins its document to another
draft — `uploads` follows the selected key, the document does not.
- SETTLE IN PLACE. The write-back replaces the placeholder where the user last
saw it instead of appending the link at the end. A card promotes to an image
when that is what arrived; the rebuild path only ever has a filename, so it
cannot know in advance.
With that, the chips are deleted outright, along with `orphanUploads` and the
three-condition rule that approximated all of the above. `interrupted` goes
too: nothing could act on it, no surface rendered it after this change, and
`isMeaningful` counted it — one dead record kept an empty draft alive for the
full TTL. The attachment's absence from the body is the signal to re-attach.
SubmitButton's `busy` now spins rather than only greying out, so an upload
with no other on-screen trace (a composer still rebuilding, a placeholder the
user deleted) does not read as a dead control.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(editor): whoever draws a placeholder registers it, not whoever finds it
Review catch on the rebuild effect. An upload started by the current mount had
its node drawn synchronously by uploadAndInsertFile, but its id only entered
`rebuiltUploadIdsRef` once the effect ran and happened to find that node. In
between, a delete (Cmd+Z right after a paste) left the effect looking at an
unmarked `uploading` record with no node — so it drew a second one, undoing a
removal MUL-5181 says must stick, and letting the settle land an attachment
the user had taken out.
The id is now registered where it is minted: an id handed into handleUpload
means the editor already drew the node. The window is sub-frame and needs a
keystroke inside one render pass, but "whoever draws it registers it" is a
rule, where "the effect will notice in time" was a race.
The composer mocks called `onUploadFile(file)` with no id, so they were not
exercising the one-id contract at all — every mount-started upload looked
inherited to the hook. They now mint and pass one like the real handle does,
which is what lets the new regression test see the difference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b5f19adbab |
feat(composer): post-send caret policy per surface (afterAccepted) (#6014)
* feat(composer): post-send caret policy per surface (afterAccepted)
Where the caret goes after a send was hand-rolled per composer: chat blurred,
quick-create hand-wrote its own requestAnimationFrame focus, and comment/reply
did nothing at all. The mechanics are identical everywhere and easy to get
wrong (must run after the clear, must survive a dialog focus trap, must not
steal focus the user moved elsewhere mid-flight), so they now live once in the
shared send contract.
`useComposerSubmit` gains `afterAccepted` ("refocus" | "blur" | "none",
default "none") plus an optional `containerRef` that bounds focus reclaim to
the composer that sent. The mode may be a function so a surface can decide at
accept time — chat only reclaims focus when the commit actually scrubbed the
shared editor, never when a fire-and-forget send left another session's draft
on screen.
Per-surface policy:
- Chat refocuses (one file, three mount points: chat page, floating window,
agent creation studio). Replaces the deliberate blur.
- Thread replies refocus — the user is mid-conversation.
- A top-level comment blurs, and IssueDetail reveals what was posted instead:
submitComment now returns the created id, and the page scrolls to that row
and flashes it with the same jumpToThread the inbox deep-link uses.
- Quick create's keep-open mode drops its hand-written rAF for the option.
- Inline comment edit and Create Issue keep "none": both close on save.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(composer): drop the scroll-to-posted-comment reveal
The top-level composer is sticky at the bottom of the timeline, so a comment
posted from it lands directly above the box and is almost always already on
screen — the scroll was a no-op and the flash re-announced something the user
had just deliberately done. The flash earns its keep for inbox deep-links,
where the user did not choose the landing spot.
`submitComment` goes back to returning a boolean; it only carried the created
id to feed the reveal. The composer still blurs after posting: the turn is
over, so the caret is dropped rather than kept.
The shared contract is untouched — `afterAccepted` never knew about comments,
which is why removing this costs nothing outside IssueDetail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(composer): drop stale references to the removed comment reveal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(composer): bind the post-send caret policy to an actually-cleared editor
Review blocker: CommentInput passed a literal `afterAccepted: "blur"`, but its
stale-submit guard declines to clear when the user typed during the request.
Posting comment A on a slow connection while typing comment B therefore
dropped the caret out of B mid-sentence — the guard kept the text, and the
blur fired anyway.
Both issue composers now resolve the mode from a ref set only on the branch
that really wipes the editor, matching what ChatInput and quick-create already
do. ReplyInput gets the same treatment: refocusing a box the user is typing in
happens to be harmless, but leaving one surface on the unsafe shape invites the
next one to copy it.
The rule is now stated on the option itself: a surface whose `onAccepted` can
decline to clear must pass a function and resolve to "none" on those paths.
Both regressions are mutation-verified — reverting either binding fails the new
assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
77b309a5ac |
feat(drafts): unified draft lifecycle + upload ownership inversion (MUL-5181) (#5900)
* feat(drafts): unified draft lifecycle + upload ownership inversion (MUL-5181) Unify how every composer preserves unsent work, sends, and handles uploads. L1 foundation (packages/core/drafts): - createDraftStore factory + self-registering cleanup-registry replacing the hand-maintained WORKSPACE_SCOPED_KEYS list; register-all-drafts guarantees registration completeness. Fixes the confirmed cross-user draft leak (persistence + in-memory) on logout / workspace delete. L3 send paradigm: - useComposerSubmit: one await-then-render contract (lock/spin, keep-on-fail, clear-on-success, single-flight, submit-time upload-gate), adopted by comment/reply/edit, create-issue, quick-create, and chat. Per-surface: - Comment/Reply/Edit: attachments moved into the persisted draft. - Create Issue: draft split into shared/manual/agent/activeMode with non-destructive mode switching + migration for old flat drafts. - Chat: optimistic send converted to await-then-render (kept server-driven cancel restore_to_input); chat draft keys registered for cleanup. L2 upload coordinator (ownership inversion, Linear-validated shape): - upload-coordinator + DraftUpload placeholder: uploads owned by a module coordinator that outlives the component, state persisted in the draft; AbortController + abort-on-logout; interrupted-on-reload. Comment surface fully wired. Create-issue/chat upload wiring is a documented residual. Verified: core + views typecheck clean; core 1064 + views 2928 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(drafts): close three review gaps in the unified draft lifecycle (MUL-5181) 1. Logout resurrection: reset in-memory draft stores BEFORE removing their persisted keys — each reset is a setState and persist writes it straight back under the still-active slug, so the old order re-created the deleted keys. The issue draft store's reset is now a full reset including lastAssignee, which clearDraft deliberately re-seeds and would otherwise hand the previous user's last-picked assignee to the next login. 2. Submit gate blind spot: the composer gate now also reads the draft's coordinator-owned upload placeholders (hasUploadingDraft). A composer reopened over a still-in-flight upload could previously send past the editor-only gate, clearing the draft out from under the settling upload. 3. Attachment binding returns to reference-filtering: a submit binds only uploads the body references, so deleting an inline image really unbinds it. An upload that settles after its mount died gets its markdown link written back into the body instead — via the reopened composer's live editor (new ContentEditorRef.insertMarkdownAtEnd) or appended to the persisted draft (new appendToDraftContent) — so close-surviving files stay visible, deletable, and honestly bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): harden upload write-back delivery after independent review Review of the previous commit (fresh-context reviewer + probe against real @tiptap/react) found the write-back could still lose a file: - insertMarkdownAtEnd now returns a boolean: the imperative handle exists from first commit but the Tiptap instance arrives in a passive effect, so an insert in that window (or after destroy) no-ops. Callers previously assumed it landed. - Write-back is now confirmed delivery (deliverFinishedUpload): insert into the live editor and, on success, persist the same body as insurance against the debounced emit being dropped by a quick unmount; append to the store only when NO composer is mounted (a mounted editor's first emit would erase a store-only append); retry while a mounted composer's instance is still warming up. Every attempt re-checks the generation guard and the body reference. - mountedRef flips in a layout effect: React nulls the child editor ref in the unmount commit, and a settle in the gap before passive cleanup saw "mounted" with no editor left to swap. - uploadAndInsertFile guards editor.isDestroyed after the await: now that uploads outlive mounts, the swap/remove paths could dispatch against a destroyed EditorView and escape as an unhandled rejection. - Tests: the reopened-composer test now asserts the editor actually received the insert (it previously passed with liveEditors disabled), plus a warming-up retry case; the mock editor mirrors isDestroyed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(drafts): roll coordinated uploads out to issue-create and chat (MUL-5181 L2) Completes the upload-ownership layer for every composer surface. The generic engine is extracted from the comment implementation into editor/use-coordinated-uploads (UploadDraftBinding adapter: store-backed accessors + registry key + body append), and use-comment-uploads becomes a thin binding over it — behavior unchanged, all comment tests green. Issue-create (manual + agent panels): - shared.attachments migrates Attachment[] -> DraftUpload[]; load normalizes legacy bare rows to `uploaded` and coerces stale `uploading` to `interrupted`. - Uploads are coordinator-owned: placeholder at pick time, survives dialog close, aborts on logout, chips for uploading/failed/interrupted, combined gate on Create and both mode-switch actions. - Write-back targets the body of the MODE that started the upload (manual description vs agent prompt); mount-time prune keeps placeholders and drops only unreferenced `uploaded` entries. Chat (tab + floating window): - inputDraftAttachments migrates to DraftUpload[] with load-time normalization; new store ops (add/settle/fail/remove upload, append-to- draft) mirror the comment store. - ChatInput adopts the engine; the upload target is snapshotted at pick time via resolveUploadTarget so a file dropped while the editor is pinned to a previous session's document files under THAT draft. - uploadMapRef is gone — the draft's uploads are the single binding source, reference-filtered at send. Hosts no longer own transport: onUploadFile prop becomes uploadEnabled, and the controller/window drop uploadWithToast. - commitDraft prunes only `uploaded` entries the body no longer references; placeholders survive keystrokes (chips are their only UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): harden L2 rollout after independent review - attachmentToDraftUpload now strips the response-scoped signed download_url before the row is persisted (draft uploads survive restarts; a stale signature 403s the preview on reopen). Covers comments, issue-create, and chat in one place; issue-create's settle reuses the helper, and the Signature assertion the rollout had dropped is restored. - chat's live-editor registry follows the LOADED draft key (reactive mirror of editorDraftKeyRef): a settle for draft B must not insert into an editor still pinned to draft A's document. - removeUpload aborts an in-flight request before dropping its placeholder. - issue-create hasDraft counts only uploaded/uploading entries so a failed remnant can't pin the sidebar draft dot forever. - Tests: mutation-proof coverage for the two placeholder-preservation rules (create-issue mount prune, chat commitDraft prune) — both previously survived rule inversion; direct core tests for the five new chat store upload ops incl. persistence and signed-URL stripping; quick-create test gets the editor i18n namespace; dead uploadWithToast scaffolding removed from both modal tests; chat-input mock aligned with the real append semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): close third-round review gaps in the upload engine - The live-editor registry registers in a layout effect: chat's adopt swaps the editor's document and loaded key synchronously during commit, and a passive re-registration one task later left a settle window where the old key mapped to an editor already holding another draft's document. The registry key is also built only when a binding exists. - removeUpload aborts only a request THIS surface tracks as `uploading` (guarded before the abort), with the comment now honest about the path being defensive — no current chip exposes ✕ mid-upload. - Mutation-proof test for the loaded-key registry rule: a dead mount's settle for a pinned draft must insert into the editor HOLDING it, not the selected one (verified to fail with the registry keyed by selection). - hasDraft upload semantics pinned by tests (uploaded/uploading count; failed/interrupted remnants don't pin the sidebar dot). - Dead scaffolding dropped: identity use-file-upload mocks and a redundant assertion in the modal tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): stale-submit draft guard + registry layout timing (review BLOCKED items) Blocker 1 — a submit that outlives its composer may only consume the draft it submitted (MUL-5181 P0). Every accepted-submit clear is now guarded: - create-issue / quick-create snapshot the singleton draft's object identity at submit; a dead panel clears (and records last-assignee/mode) only if the draft is untouched, and never runs close/reset effects. A replaced draft B typed after close survives a late success of draft A. - comment / reply / edit snapshot the per-key draft entry; a dead composer clears only the exact entry it submitted. - chat snapshots the sent slot's value; a dead mount's commitInput clears only an unreplaced draft. Mutation-verified tests for the create panels and comments (guard inverted => tests fail), plus untouched-draft control cases. Blocker 2 — the live-editor registry is now genuinely registered in a layout effect. The prior commit claimed this fix but a test-time `git checkout --` discarded the unstaged engine edits before committing; re-applied: layout registration, binding-gated registry key, and the tracked-only abort in removeUpload. New registry timing test captures the registry from a parent layout effect across a key switch — verified to fail with passive registration. Also: `multica:chat:selectedProjectId` joins the workspace-scoped cleanup list (was leaking across logout; flagged as a pre-existing risk). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): mounted submits also clear only the draft they submitted The stale-submit snapshot guard previously protected only dead composers; a mounted one cleared unconditionally on success. But the editor stays interactive during a request (Tiptap cannot toggle editable post-mount), so text typed while draft A was in flight was wiped by A's success. The guard is now unconditional across every surface: success consumes exactly the submitted snapshot, and any later edit survives. - create-issue / quick-create: the editor's pending debounce is flushed into the store BEFORE snapshotting (a late flush of pre-submit typing must not read as a mid-flight edit); a touched draft skips clear AND close/reset — the dialog stays open on the newer work. Untouched behavior unchanged. - comment / reply / edit: same flush + snapshot; a touched entry keeps both the store draft and the editor content (edit mode stays open on it). - chat: commitInput's value compare now applies while mounted too, and the editor is scrubbed only for an untouched draft. - use-composer-submit docs no longer claim "editor locked": they state the real contract — send affordance locks, edits after submit survive. Regression tests: mounted mid-flight-edit cases for manual create (incl. "dialog must not close over draft B"), quick create, comment, and chat, plus mounted-untouched controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): idempotent draft writes so a tab switch cannot resurrect a posted comment Final-review blocker: the comment/reply visibilitychange/pagehide flush re-writes IDENTICAL content on every tab switch, and writeDraft minted a new entry object each call — the stale-submit guard's identity compare then read a mid-flight tab switch as "edited during the request", kept the posted comment's draft alive, and left Send enabled for a duplicate. - writeDraft is now a no-op when content and uploads are unchanged (also kills a spurious persist write per tab switch). Regression tests: entry identity preserved on identical setDraft (core), and the reproduced tab-switch-mid-send scenario clears the posted draft (views) — verified to fail with the idempotence removed. - onAccepted now flushes the editor's pending debounce before judging `untouched` on every surface, so typing still inside the debounce window counts as a mid-flight edit instead of being scrubbed. - create-issue records last-assignee/mode from the SUBMITTED values, outside the untouched gate — a created issue updates the preference even when the dialog stays open on newer edits. - Stale guard comments corrected in both create panels; the use-composer-submit docstring no longer claims project/feedback were migrated (they still hand-roll await-then-clear; registered debt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ceba14a227 |
fix(issues): MUL-5362 return to the source list after deleting an issue (#5997)
* fix(issues): return to the source list after deleting an issue Deleting an issue from its detail page always pushed the workspace Issues list, so opening an issue from My Issues (or a project list, search, a pin, an agent panel) and deleting it dropped the user's navigation context — GH #5995. Go back instead. `useBackOrReplace` steps back when the platform reports in-app history and replaces with a fallback path when there is none, so a shared link opened cold or a new tab never steps off the app. Web answers via the Navigation API, falling back to counting its own pushes; desktop reads the active tab's virtual history. `replace`, not `push`: the deleted issue's URL must not stay in history for the back button to land on a 404. The not-found "Back to Issues" button loses the same context, so it moves to the same helper and its label becomes a plain "Back". Co-authored-by: multica-agent <github@multica.ai> * fix(web): track history position, not push count, for canGoBack The Navigation API fallback only ever counted pushes, so `pushes > 0` did not mean the current entry still had an in-app page behind it. Cold-open an issue, click Issues, press the browser's Back button, then delete: the tracker still claimed history and `back()` would step off Multica — the exact case the fallback exists to prevent. Count depth instead: a push adds one, any traversal takes one away (clamped at zero). Reaching the document's first entry requires traversing back at least as many times as we pushed, so a positive depth can never be claimed while sitting on it. A browser Forward is now conservative — it reports no history where a step back would have been fine — which costs a fallback navigation rather than an exit. Also corrects the useBackOrReplace contract comment: stepping back leaves the dead URL in forward history, so the guarantee is that it never lands on the back stack, not that it leaves history entirely. Co-authored-by: multica-agent <github@multica.ai> * fix(web): answer canGoBack from the browser alone, never from push counts Review found the counter still lied, one level deeper: it counted `router.push` calls, and a call is not a committed history entry. Next drops a push to `replaceState` when the canonical URL is unchanged (app-router.js, the `pendingPush && href !== canonicalUrl` branch), so clicking a self-link — the breadcrumb on an issue detail page, a pin to the issue you are on — incremented depth with no entry behind it, and a delete from there could still step off the app. Fixing the count needs per-entry markers, which means depending on both React effect ordering (our provider's effect runs before app-router's history commit) and Next's own preserveCustomHistoryState behaviour. Two rounds of review have now found holes in hand-rolled history tracking; a third layer of it is not the way to buy this guarantee. So stop deriving. `canGoBack` is the Navigation API's answer or `false`. Browsers without it take the fallback path, which is exactly what they did before any of this existed — nobody regresses, and the "wrong true walks the user out of the app" failure is now unreachable by construction. Drops the tracker, the popstate wiring and the push wrapper: the `multica:navigate` bridge returns to its original shape. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
999f73e61f |
fix(projects): show owner/repo label for GitHub resource URLs (MUL-5221) (#5795)
* fix(projects): show owner/repo label for GitHub resource URLs instead of truncated full URL Long GitHub URLs in the project-resources sidebar are end-truncated by CSS `truncate`, making similar repos like org/very-long-repo-a and org/very-long-repo-b indistinguishable from the visible prefix alone. Extract just the `owner/repo` segment from the URL for display; keep the full URL in the tooltip so it remains accessible on hover. Applied to both the resource row and the repository picker popover. Custom labels are unaffected. Fixes #5771 Co-authored-by: multica-agent <github@multica.ai> * fix(projects): strip .git suffix and handle scp SSH URLs in githubShortLabel; add middle-truncation for long repo names Addresses #5795 review feedback: - Strip .git suffix (example-org/very-long-repo-name.git → without .git) - Handle git@github.com:owner/repo.git (scp shorthand) which new URL() rejects - Add midTruncate() so repos sharing a long common prefix are distinguishable by their tail (customer-alpha vs customer-beta) rather than being end-truncated identically by CSS Fixes #5771. Co-authored-by: multica-agent <github@multica.ai> * refactor(projects): share githubShortLabel across project resource UIs Move githubShortLabel/midTruncate out of project-resources-section into packages/views/common/github-url.ts so the create-project repository picker reuses the same owner/repo shortening (it was still rendering full URLs) and recognize the www.github.com host. Add github-url.test.ts covering the https / ssh / scp / enterprise / malformed cases plus middle-truncation. Addresses #5795 review feedback: shared helper, create-project coverage, tests. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> |
||
|
|
d4391fda4d |
fix: preserve new issue draft options (#5770)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
645e8a2983 |
feat(issues): 确认指派弹窗去掉打开时的 run 预览等待 (MUL-5010) (#5678)
* feat(issues): confirm assignment without a pre-flight run preview (MUL-5010) RunConfirmModal called POST /api/issues/preview-trigger on open and blocked the entire dialog behind a "检查中…" spinner until it landed. That query is keyed per issue id with staleTime 0, so every new issue was a guaranteed cache miss — the wait was structural, not incidental. The dialog also promised "会立即开始工作" while still checking whether anything would start. Redefine it as "dialog = confirm the assignment", not "confirm N runs": - Open fires no request. Note box and both buttons are usable on frame one. - Title/primary action become 确认指派; copy states the assignment as certain and the run as conditional, with no predicted count. - The write reports what actually happened: UpdateIssue returns runs_started (0/1) and batch-update returns the batch total, surfaced as one short toast — "已指派给 X,已启动 N 个 run", or just "已指派给 X". runs_started counts successful enqueues rather than predicate hits, since an insert can still no-op on the pending unique index. It is pointer+omitempty like Labels, so only the PUT reply carries it, and it is stripped in useUpdateIssue's onSuccess so a write-scoped fact never lands in the cache. The handoff-note version gate stays local and synchronous, now covering squads too by resolving the leader's runtime from the warm squad list — removing the last reason the dialog needed the server round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): assert runs_started on the write responses (MUL-5010) The assign-confirm dialog now reports the enqueue outcome from the write reply instead of predicting it, so the number itself is the contract. Pin it directly on the responses: - single assign that enqueues → runs_started 1 - single assign that starts nothing → 0, via both routes (suppress_run, and assigning into backlog), so a client can tell "no run" apart from "old backend omitting the field" - batch → the real aggregate, asserted against a mixed selection where updated=3 but runs_started=2; the divergence from `updated` is the point - absent on GET, guarding the pointer+omitempty contract that keeps a write-scoped fact off every read path Each count is cross-checked against agent_task_queue so the assertions track real enqueues rather than the predicate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): drop runs_started + result toast, keep silent completion (MUL-5010) Final scope: the dialog only removes the preview wait; it does not add a post-submit result toast. Per the confirmed decision, revert everything that existed solely to report "N runs started" and keep completion silent, exactly as it was before this PR — the assignee and any run already surface through the issue's normal updates. Removed: - the success toast on submit and its two i18n keys (all four locales) - the `runs_started` response field on IssueResponse, the single/batch write counting, and dispatchIssueRun's bool return (back to void) - the `runs_started` type/schema/api-client additions and the onSuccess strip - server/internal/handler/issue_runs_started_test.go (only served the field) Kept: preview-trigger removal and the instant-usable dialog — note box and buttons live on the first frame, agent/squad handoff-note version gate resolved locally, and "确认指派" copy. Net diff vs base is now frontend-only; the backend and packages/core files are byte-identical to base again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs(issues): fix stale headline comment referencing removed result toast (MUL-5010) The result toast was removed with runs_started, but the headline comment still said "the real one arrives in the result toast". Reword to state only what remains true: the copy names no run count because the run is conditional. 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> |
||
|
|
fae7b046f2 |
feat(views): support the send shortcut in manual issue create (MUL-4931) (#5583)
* feat(views): support the send shortcut in manual issue create (MUL-4931) Agent create has had Cmd/Ctrl+Enter all along; manual create had no submit shortcut at all, in either the title or the description. Reuse the configurable `send` action rather than hardcoding the chord, so a rebound or unbound shortcut follows the user's setting and the IME guards and Shift+Enter replay come along for free: - Description: pass `onSubmit` to ContentEditor, which already carries the extension. - Title: add an opt-in `onSubmitShortcut` to the shared TitleEditor. Plain Enter stays inert there — #5532 removed that trigger a day ago because it created from half-typed titles — and a single-line title has no newline to trade Enter against, so the chord path refuses a plain-Enter `send` binding. Hosts relying on plain-Enter submit (create-project, autopilot-dialog) keep `onSubmit` and are untouched. Also fixes a real double-create: `submitting` is state, so two chord presses in one tick both read the stale value and fired two creates. A ref flips synchronously and single-flights both create panels. Accessibility: the empty-title Create button moves from native `disabled` to `aria-disabled`, so it stays focusable and keyboard/SR users can finally reach the "Enter a title to create" tooltip. Keycaps are decorative, keeping the accessible name "Create Issue". Empty-title chord submits now focus the title instead of silently no-oping. Widen the `send` setting description across all four locales — it claimed to cover only chat, comments, replies, feedback and prompts. Tests: real-ProseMirror coverage for the title chord (every other editor test mocks Tiptap, so nothing verified extension ordering), plus chord/plain-Enter/ empty-title/upload-gate/single-flight/keycaps/aria-disabled cases. The single-flight test dispatches both presses inside one act(); it fails against the old state-based guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(views): cover quick-create single-flight + aria-disabled visuals (MUL-4931) Review follow-up on #5583. The quick-create ref guard shipped without a regression test, so the claim that both create panels were covered was only true of manual create. That path files a real issue, so a double-fire is a duplicate issue rather than a glitch. Add the same-tick regression: both presses dispatch inside one act(), and it fails against the old state-based guard (expected 1 call, got 2). The Button base only styles native `disabled` (disabled:opacity-50 / disabled:pointer-events-none), so the aria-disabled empty-title button stayed a fully lit, pressable-looking primary. Add local aria-disabled opacity, cursor, and press-animation styles, with no pointer-events-none — that would break the tooltip hover and the click that focuses the title. Verified against compiled Tailwind output: aria-disabled:active:translate-y-0 and the base's active:not-aria-[haspopup]:translate-y-px have equal specificity and the override emits later, so it wins without !important. 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> |
||
|
|
82906bd9e9 |
feat(settings): move create-issue field config into a Settings Issue tab (#5556)
The quick-create field toggles used to live inside the quick create popover; per feedback on MUL-4873 they belong in the Settings page. Adds a My Account 'Issue' tab with one group per create mode backed by a new per-workspace issue-create-settings store, extends field visibility to the manual dialog (status/priority/assignee/labels/ project/due date/start date with value-reveal and overflow re-entry), and routes both dialogs' 'Customize fields' items to the tab. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ed9adc2bbe |
feat: improve create issue field controls (#5532)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fcb27a729 |
fix(issues): attach labels in the create transaction (MUL-4832) (#5510)
* fix(issues): attach labels in the create transaction (MUL-4832) Labels chosen at issue creation were attached in a second, non-atomic round-trip after the issue was already committed (web modal looped attachLabelToIssue per label; the create endpoint took no labels). A partial failure of that follow-up left the issue committed but mis-categorized, surfaced only as a toast. Carry label_ids through the create request instead: the service validates them (workspace + resource_type='issue') and attaches them inside the same transaction as the issue insert, so the issue and its labels commit together or not at all. An unknown or wrong-scope label id now fails the whole create with 400 rather than being silently dropped. Duplicate ids are idempotent (dedupe + ON CONFLICT DO NOTHING). - server: IssueCreateParams.LabelIDs + ErrIssueLabelNotFound; validate and attach in IssueService.Create; handler parses label_ids and maps the error to 400. - web: create-issue modal forwards label_ids and drops the post-create attach loop and its dead toast key. - tests: handler coverage for atomic attach, stale-id 400 (no issue left behind), duplicate-id idempotency, wrong-scope rejection; web test asserts label_ids is forwarded. Scope is deliberately labels-only. The contributor PR #5475 also auto-parsed an acceptance_criteria field from description text; that introduces a new user-facing data contract with no defined edit/display rules and is left out for a separate product decision. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): echo created labels + guard old-backend compat (MUL-4832) Addresses review of #5510. 1. Create response + issue:created event now carry the labels attached in the create transaction. IssueService.Create returns the authoritative label snapshot; the handler sets it on both the HTTP response and the issue:created broadcast payload. Without this, online members other than the creator saw the new issue unlabeled indefinitely (staleTime: Infinity, no invalidation) because this PR removed the old post-create issue_labels:changed broadcast. 2. New web + old backend compatibility. During the rolling deploy window the web app can run ahead of the backend (web auto-deploys on merge, backend deploys manually). An older backend ignores label_ids and returns an issue with no labels field. The create modal now falls back to the legacy per-label attach only when the response omits labels, so labels aren't silently dropped; when labels is present the atomic path already ran and no fallback fires. The backend always returns an explicit labels array (empty when none) as the detection signal. Tests: handler issue:created-carries-labels + response-carries-labels + response-always-includes-labels; web modal no-fallback (new backend) and fallback (old backend); ws-updaters keeps the label snapshot in list cache. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): validate create response through a schema (MUL-4832) Addresses review of #5510 (must-fix 3). The create modal keys its label-attach compatibility fallback off `issue.labels` being absent (older backend) vs a validated Label[] (current backend). But api.createIssue cast the raw JSON straight to Issue, so a malformed labels value (null, an object, a garbage array) would be !== undefined and wrongly suppress the fallback, caching a bad shape — violating the repo's API Compatibility rule. Parse the create response through CreateIssueResponseSchema: - labels absent -> undefined (older-backend signal; fallback runs) - labels valid -> Label[] (fully validated elements, not z.unknown()) - labels malformed -> undefined via .catch (safe: never masquerades as handled; worst case a redundant re-attach, never a silent drop) A whole-body parse failure degrades to EMPTY_ISSUE (never throws into React), matching the existing parseWithFallback contract. Tests: schema.test.ts covers absent / valid / null / wrong-element-shape labels and the empty-issue degrade. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): reject a malformed create response instead of faking success (MUL-4832) Follow-up to review of #5510. The prior fix degraded a schema-failed create response to EMPTY_ISSUE, which React Query treats as a successful mutation: the empty issue is written into the list cache, a blank "created" toast shows, the "View issue" link points at an empty id, and with labels the fallback attach runs against an empty issue id. A create that returns an unusable body is a failed mutation, not a safe-empty read. Fall back to null and reject: mutateAsync is already inside the create modal's try/catch, so a controlled rejection preserves the draft and shows the failure toast, and onSettled still refreshes the list so a genuinely-created issue can still surface. - CreateIssueResponseSchema tightens id to non-empty; an id-less body routes to the same reject path. - createIssue throws on parse failure (empty-message Error so the modal renders its localized "failed to create" toast; parseWithFallback already logged the schema issues + raw body). - Dropped the EMPTY_ISSUE fallback constant. - Tests: whole-body malformed and empty-id now assert createIssue rejects; only-labels-malformed still returns the real issue with labels undefined. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f9f41e991a |
fix(ui): drop native title that stacked on custom hover surfaces (MUL-4836) (#5502)
ActorAvatarBase hardcoded `title={name}` on the avatar div, so every one of
the 155 avatar renders emitted a browser tooltip. On the 34 call sites that
also pass `enableHoverCard`, the native title sat on the inner div while the
PreviewCard trigger sat on an ancestor span — different elements, so neither
suppressed the other and both surfaces showed at once.
Same class of bug in two more places: AgentStatusDot added a third `title`
inside the same trigger on the 16 sites that also pass `showStatusDot`, and
RepoUrlText rendered a `title` carrying the exact string its Tooltip already
showed.
Phase 1 scope only — remove the three duplicate titles. PreviewCard trigger
semantics, `profileLink`, ancestor detection, the 159 call sites, and the
remaining native-title cleanup are deliberately untouched.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
3b08456264 |
feat(views): block draft-fixing actions while attachments upload (#5465)
Submitting a composer mid-upload silently lost the file: the editor holds a `blob:` placeholder that gets stripped during serialization, and the attachment id does not exist yet, so the send succeeded without the file. Chat and Quick Create gated this; manual issue create, Feedback's button, comments, replies and comment edit did not. Gate every action that FIXES a draft — Send/Create/Save, Cmd/Ctrl+Enter, Enter on the title, and the manual/agent mode switches — behind one source of truth: the editor document, which IS the upload queue. ContentEditor publishes queue transitions via `onUploadingChange` (a transaction listener, not `onUpdate` — that path is debounced and skips no-change emissions, and a failed upload leaves byte-identical markdown, so the un-gate would never fire). `useUploadGate` pairs that render state with an `isBlocked()` re-read at submit time, since the shortcut paths never consult the button. The publisher emits its current answer on subscribe rather than only on flips: hosts outlive editor instances (comment edit remounts on cancel, chat swaps by key on agent switch), and an editor torn down mid-upload would otherwise leave submit wedged shut with no pending node left to reopen it. Also fixes the failure toast that never fired: `uploadWithToast` only reported errors if a caller passed `onError`, and none did, so failed uploads removed their placeholder and vanished unexplained. `useEditorUpload` now supplies it once for every composer. Per MUL-4808: drops Quick Create's upload-time lock on the attach button (files can queue), and leaves description autosave ungated by design. 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> |
||
|
|
b64ebd60b5 | feat: add customizable keyboard shortcuts (#5294) | ||
|
|
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> |
||
|
|
c6783efd88 |
feat(views): unify avatar upload with crop editing (#5074)
* feat(views): unify avatar upload with crop editing across web/desktop Add a shared AvatarUploadControl + AvatarCropDialog used by the user, workspace, agent, and squad avatar entry points. Cropping (pan/zoom, fixed 1:1) and compression run client-side on canvas; the existing /api/upload-file + avatar_url chain is reused unchanged (no backend/API/DB changes). This collapses four hand-rolled upload buttons into one control and removes AvatarPicker. Also make the shared display avatar treat all non-human actors (agent, squad, system) as rounded squares — completing the "circles are for humans" convention the editors already assumed, so display and editors agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(views): rebuild avatar cropper to the "Edit avatar" reference form Replace the hand-rolled canvas cropper with react-easy-crop to match the requested design: full-bleed image with a dimmed overlay outside a bright crop window, a rotate control, a zoom slider flanked by −/+, and a Reset / Cancel / Save footer. Round window for people, rounded-square for non-human actors; output stays a 512px square (webp, jpeg fallback) through the same upload/avatar_url chain. avatar-crop.ts keeps the encode pipeline and gains rotation-aware getCroppedAvatarBlob; the interactive geometry now lives in react-easy-crop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): round square avatar crop frame --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3cb5dc3ad6 |
chore(analytics): retire redundant PostHog tracking (MUL-4127) (#4996)
* chore(analytics): retire redundant PostHog tracking (MUL-4127) PostHog had become a chaotic, largely-unused second copy of data we already query from the DB and Grafana. Remove the redundant instrumentation. Server: every product event (signup, workspace_created, issue_created, issue_executed, chat_message_sent, team_invite_*, onboarding_*, agent_created, cloud_waitlist_joined, feedback_submitted, contact_sales_submitted, squad_created, autopilot_created) is now in metricsOnlyEvents, so metrics.RecordEvent still increments the Prometheus/Grafana counter but no longer ships to PostHog. DB rows remain the source of truth. Runtime/autopilot/ agent_task lifecycle were already Prometheus-only. Frontend: delete the PostHog-only funnel instrumentation — $pageview (+ web and desktop trackers), download_intent_expressed/page_viewed/initiated, the onboarding_started mirror, onboarding_runtime_path_selected/detected, feedback_opened, and source_backfill_*. The source-backfill modal itself stays (it PATCHes the questionnaire to the DB). Kept on PostHog (frontend only): $exception autocapture and the client_crash / client_unresponsive stability telemetry (no DB equivalent), plus $identify/$set. captureSignupSource (attribution cookie) stays — it still feeds the signup_source Prometheus label. Verified: pnpm typecheck, pnpm lint (0 errors), vitest (core/views/web/desktop), go test ./internal/analytics/... ./internal/metrics/... Co-authored-by: multica-agent <github@multica.ai> * docs(analytics): fix stale PostHog references after MUL-4127 (review follow-up) Addresses review of #4996 — three spots still described server events as active PostHog signals after they became metrics-only: - docs/analytics.md: issue_executed is no longer a PostHog success signal; it is Prometheus-only (multica_issue_executed_total) + issue.first_executed_at, in both the event contract and the Reconciliation section. - docs/analytics.md: the signup $set_once person properties (email, signup_source) are no longer emitted — signup is Prometheus-only; only the bucketed signup_source survives as the multica_signup_total label. - server/internal/metrics/business_events.go: RecordEvent doc comment no longer claims it ships product events to PostHog / "PostHog is reserved for user/product-behaviour events" — every server event is now metrics-only. Co-authored-by: multica-agent <github@multica.ai> --------- 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> |
||
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 review nits)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|
||
|
|
03828015ca |
fix(feedback): validate response and pass error kind (#4633)
Wire structured feedback kind through the frontend/core feedback path so desktop route-renderer errors submit as bug feedback, and bring the feedback client onto parseWithFallback. MUL-3768 |
||
|
|
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> |
||
|
|
f0d6d88069 |
fix(views): resolve handoff-note version gate locally for direct agent assigns (#4585)
The run-confirm interception box gated its handoff-note field on the preview round-trip's `handoff_supported`, so every open showed a "checking…" wait before the note box could even be used — to learn something the client already holds. For a concrete agent assignee the target runtime is exactly that agent's, and its CLI version is already warm in the prefetched agent + runtime caches, so the box can settle synchronously, the same way the quick-create version gate does. Add a frontend `handoffSupported` mirror of the server's MinHandoffCLIVersion gate, resolve the agent → runtime → cli_version locally, and drive the note box from that verdict without waiting on loading. Squad / batch-status / unresolved-agent paths — whose resolved trigger set is only known server-side — keep falling back to the preview's `handoff_supported`, unchanged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
57d1a0a00f |
fix(quick-create): track concurrent uploads with in-flight counter (MUL-3339) (#4562)
`useFileUpload` exposed a single `uploading: boolean` shared across all
concurrent upload calls. When the user drag-dropped N images into the
Quick Create modal, the first upload's `finally` flipped the flag back
to false while N-1 uploads were still in flight. The submit gate (which
only checked `uploading`) re-enabled, and on submit:
- `getMarkdown()` ran `stripBlobUrls()` and erased every still-pending
`` placeholder from the prompt.
- `pendingAttachments` only contained the first-completed upload, so
`activeAttachmentIds` shipped a single ID.
- The remaining attachment rows never got linked to the new issue —
their `issue_id` stayed NULL and the UI showed "Attachment doesn't
exist".
Fix:
1. Replace the boolean with an in-flight counter in `useFileUpload`.
`uploading = inFlight > 0` so the flag stays true until ALL concurrent
uploads resolve (or reject — the `finally` decrements either way).
The public return shape is unchanged; every existing call site keeps
working.
2. Belt-and-suspenders: add `editorRef.current?.hasActiveUploads()` to
the quick-create submit gate. The editor already tracks per-node
`uploading` attrs (the source of truth for "is there a blob preview
still resolving"); checking it on submit guarantees the strip step
can never run against an in-flight image even if some future code
path mis-clears `uploading`.
3. Regression coverage in `use-file-upload.test.ts`:
- Fire two concurrent uploads, resolve them out of order, assert
`uploading` stays true until BOTH resolve. Confirmed to fail
against the pre-fix code.
- Reject one of the concurrent uploads, assert the counter still
decrements correctly (the `finally` runs on rejection).
The mock ContentEditor in `quick-create-issue.test.tsx` gains a
`hasActiveUploads: () => false` stub so the new defense call doesn't
explode in existing tests.
Verified: `pnpm test` on @multica/core (663 tests) and @multica/views
(1471 tests) both green; typecheck + lint clean on both packages.
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
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> |
||
|
|
a123dfc2df |
MUL-3508: stage sub-issues so the parent wakes per stage, not per child (#4410)
* feat(issues): stage sub-issues so the parent wakes per stage, not per child Sub-issues under a parent can be grouped into ordered stages (issue.stage). The child-done -> parent notification + assignee wake now fire only when a stage barrier closes: every sub-issue in the lowest unfinished stage has reached a terminal status (done/cancelled). An unstaged sibling set is one implicit stage, so the parent is woken once when the last sub-issue finishes instead of on every child — the default fix for the fire-on-every-child cascade reported in discussion #4320 / MUL-3508. Stage advancement stays agent-driven: the server only detects the closed barrier and wakes the parent assignee, who decides whether to promote the next stage. - DB: nullable issue.stage (CHECK >= 1) + sqlc regen - API: stage on issue create/update/response and batch update - CLI: `issue create`/`issue update` --stage; new `issue children` command that lists sub-issues grouped by stage (table + json) - stageBarrierClosed / stageProgressSummary in issue_child_done.go, with the wake comment now stage-aware, plus unit tests - skill docs (multica-working-on-issues SKILL.md + source map) Web UI (create-form stage picker, sidebar edit, group-by-stage display) is a follow-up; the API already returns stage for it to consume. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address review on stage barrier (cancel, batch, unstaged) Resolves the three blockers from the PR review: 1. Cancel can close a stage. The child-done barrier now fires on any non-terminal -> terminal transition (done OR cancelled), not just done. isTerminalChildStatus already treats cancelled as terminal (a cancelled sibling never finishes, so it must not hold a stage open), so a cancelled last-open child now closes its stage and wakes the parent. Keying on the transition also makes a later cancelled -> done edit a no-op, avoiding a lagging duplicate wake. 2. Batch update of stage no longer no-ops. `hasMutation` now includes "stage", so `{"updates":{"stage":N}}` persists instead of returning {"updated": 0}. 3. Unstaged children no longer participate in the staged frontier. In a staged sibling set, NULL-stage children neither hold a stage open nor fire on their own completion, and the wake comment no longer renders "Stage 0". This matches migration 123 ("NULL does not participate in staged grouping") and the CLI's separate unstaged group, removing the footgun where an unstaged backlog child silently blocked Stage 1. Tests: cancellation closes a stage (staged + unstaged), unstaged ignored in a staged set, stage summary skips unstaged, and a stage-only batch update persists. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * feat(web): stage UI — create picker, sidebar edit, group sub-issues by stage Frontend for the sub-issue stage feature (web + desktop, shared via packages): - core: `stage` on the Issue type + create/update request types; zod IssueSchema parses it (defaults to null for older backends) with schema tests for the numeric and omitted cases. - StagePicker component (mirrors the other property pickers): "No stage" + Stage 1..N, offering one beyond the current/sibling max. - Create-issue modal: a Stage pill, shown only when a parent is selected, threaded into the create payload. - Issue detail sidebar: an editable Stage row + "add property" entry, gated to sub-issues (issues with a parent). - Sub-issue list grouped by stage with per-stage headers (flat when unstaged). - i18n: stage keys across en / zh-Hans / ja / ko (parity test passes). Verified: full typecheck (6/6), core (591) + views (1433) vitest suites, lint clean (no new findings). Backend/CLI shipped earlier in this PR. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * test(issues): add stage to Issue fixtures merged from main The merge brought in new Issue fixtures that predate the required `stage` field: core issues/batch.test.ts, views batch-action-toolbar.test.tsx, and the mobile EMPTY_ISSUE_FALLBACK sentinel. Add `stage: null` so they satisfy the Issue type (mobile reuses core's IssueSchema for parsing, so only the sentinel needs it). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(web): feed StagePicker the sibling max stage so higher stages stay selectable The StagePicker accepts maxStage to extend its option list beyond the floored Stage 1-3, but neither call site passed it, so a parent with an existing Stage 4/5 child could not pick that stage when creating a new sub-issue or editing one in the sidebar. - Compute the sibling max stage at both call sites: the create modal now loads the parent's children (childIssuesOptions) and the detail sidebar reuses the already-loaded parentChildIssues. - Extract maxSiblingStage + stageOptions as pure helpers on stage-picker and unit-test them (the regression: a Stage 5 sibling keeps Stage 5 selectable and offers Stage 6). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da72e2fa22 |
feat(daemon): inject project description into the agent brief (MUL-3465) (#4395)
* feat(daemon): inject project description into the agent brief Issues bound to a project only surfaced the project title in the runtime brief; the project description (durable, project-wide context the owner sets) was loaded but dropped. Carry it end-to-end: - claim handler reads proj.Description onto the response (issue-bound and quick-create paths) - new ProjectDescription field on AgentTaskResponse, daemon Task, and TaskContextForEnv - rendered in the brief's `## Project Context` section and written to .multica/project/resources.json as project_description Empty descriptions render nothing (no extra heading). Updated the projects-and-resources built-in skill docs in the same change. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * feat(projects): clarify project description is injected as agent context The project description is now durable context injected into every task's brief, but the UI still presented it as a plain "Description" field, so existing descriptions could silently become agent input. Add a hint under the description editor on the project detail page and in the create-project modal, in all four locales, stating it is shared with agents as context for every task in the project. No data-semantics change. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * test(handler): assert project description flows through task claim The execenv tests cover brief rendering, but nothing pinned the claim handler boundary where proj.Description is read onto the response. Add two tests — issue-bound and quick-create paths — so a regression in that assignment fails loudly instead of silently dropping the description. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fa15041864 |
MUL-3254: fix pasted image draft rendering in desktop (#4066)
* fix: keep issue draft attachment records Co-authored-by: multica-agent <github@multica.ai> * fix: avoid persisting signed draft attachment urls Co-authored-by: multica-agent <github@multica.ai> * fix: reuse resolved media url for draft previews Co-authored-by: multica-agent <github@multica.ai> * fix: address draft attachment review nits - Backfill an empty caller download_url from the in-session upload on id collision so a just-pasted image first-paints from the signed URL instead of detouring through markdown_url. - Prune draft attachments no longer referenced by the persisted description when the create dialog reopens. - Backfill EMPTY_DRAFT defaults on draft-store rehydrate so drafts persisted before the attachments field existed get a stable shape. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c8ab73d38d |
MUL-3244: Bind quick-create attachments to created issues (#4062)
* fix: bind quick-create attachments to created issues Co-authored-by: multica-agent <github@multica.ai> * test: use real image markdown in quick-create attachment test Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d52c4f238f |
fix(desktop): contain renderer crashes (#3643)
* fix(desktop): contain renderer crashes Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): filter renderer exit prompts Co-authored-by: multica-agent <github@multica.ai> * refactor(desktop): drop redundant page-level ErrorBoundary on issue detail The whole-page <ErrorBoundary> wrapper duplicated the new route-level errorElement (DesktopRouteErrorPage). Let render errors bubble to the root route boundary so all detail routes are contained the same way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(desktop): add Close tab escape to route error page Reload tab recreates the same crashing path and Go to issues is a dead end when the issues route itself crashed. Add a Close tab action that destroys the crashing router entirely and falls back to a sibling tab (or a reseeded default), the only always-safe escape regardless of which route crashed. 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> |
||
|
|
90ddfb04e2 |
feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777) (#3441)
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433) When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces returns 403 for every caller and the UI hides every "Create workspace" affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This closes the gap where ALLOW_SIGNUP=false still let any signed-in user open an isolated workspace the platform admin couldn't see. - server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace, workspace_creation_disabled in /api/config, Go tests. - frontend: new workspaceCreationDisabled in configStore, hide sidebar entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding StepWorkspace to a "creation disabled, ask for invite" state when the flag is on, EN + zh-Hans locale strings. - ops: .env.example, docker-compose.selfhost, helm values + configmap, SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs (EN + zh). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): drive create path off workspaceCreationAllowed (#3433) PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already has a workspace, StepWorkspace still walked the resume copy (`headline_resume` / `lede_resume` mentioning "or start another") and `creatingActive` ignored the flag, leaving a stale clickable create CTA possible if /api/config arrived late. Refactor StepWorkspace to derive a single `workspaceCreationAllowed` boolean from the config store. It now drives: - Initial `mode` state (defaults to "existing" when disabled + reusing so the CTA is pre-armed for the only valid action). - `creatingActive` so the footer CTA cannot fall back into the create branch even mid-render. - Eyebrow / headline / lede strings — adds `creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for the disabled + reusing variant. Tests: cover the three reachable shapes — flag off + no existing, flag on + no existing, flag on + existing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bd1fb10afa |
chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute - Replace useContext() with React 19 use() across 16 context consumers - Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files (mobile excluded — Hermes lacks toSorted support) - Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check - Fix nested component definition: useMemo wrapping HeaderRight → useCallback - Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad React Doctor score: 23 → 30. No behavioral changes, no business logic modified. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
17714c3ad1 |
fix(create-issue): preserve parent_issue_id through Create with agent flow (MUL-2534) (#3083)
* fix(create-issue): preserve parent_issue_id through Create with agent flow (MUL-2534) When the create-issue modal was opened from the "Add sub issue" entry on an existing issue and the user switched to "Create with agent", the parent_issue_id was silently dropped: switchToAgent only forwarded prompt + actor + project_id, the AgentCreatePanel had no notion of parent context, and the daemon prompt never instructed the agent to pass --parent <uuid>. The sub-issue intent was lost and the new issue landed as a standalone. This fix threads parent_issue_id through the whole pipeline silently — no new editable form field, the existing carry channel handles it: - Frontend: ManualCreatePanel.switchToAgent + AgentCreatePanel.switchToManual now carry parent_issue_id (and identifier, for display) so the sub-issue intent survives mode flips in either direction. AgentCreatePanel reads parent from `data`, forwards to api.quickCreateIssue, and renders a read-only "Sub-issue of MUL-XX" chip so the user can see the relationship. - API: quickCreateIssue accepts optional parent_issue_id. - Backend: QuickCreateIssueRequest validates parent_issue_id belongs to the same workspace (same path as CreateIssue), persists it in QuickCreateContext, and the daemon claim handler resolves the parent's identifier for prompt context. - Daemon prompt: when ParentIssueID is set, buildQuickCreatePrompt instructs the agent to pass `--parent <uuid>` and treat the modal entry point as authoritative. Tests cover all three hops: switchToAgent carry payload, AgentCreatePanel → api.quickCreateIssue, and the daemon prompt's --parent injection (with both identifier-present and UUID-only fallback branches). Co-authored-by: multica-agent <github@multica.ai> * test(create-issue): cover quick-create parent trust boundary + identifier fallback (MUL-2534) Address review on PR #3083: - Add server-side test for POST /api/issues/quick-create parent_issue_id: same-workspace parent threads through QuickCreateContext.ParentIssueID, foreign-workspace and bogus UUIDs return 400 and never enqueue a task. - Fall back to `data.parent_issue_identifier` in ManualCreatePanel's switchToAgent when the parent detail query hasn't hydrated yet, so the agent chip never renders "Sub-issue of " with an empty tail. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
329fc0139d | fix(create-project): anchor Source popover to top so it doesn't get pushed below the trigger when the modal is expanded | ||
|
|
341ce7bfa5 |
feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)
Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.
Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.
Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.
Co-authored-by: multica-agent <github@multica.ai>
* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)
Addresses the Elon review on PR #3263:
- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
matching handler, CLI `project resource update`, and a new
EventProjectResourceUpdated WS event. resource_type stays immutable;
ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
embedded label differs — the row-level UNIQUE only matches the full
ref JSON, so a label typo would otherwise let the same working
directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
invalid path) and the label-shadow conflict on both create and
update; the in-place rename still succeeds because the conflict
scan ignores the row being edited.
Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.
Co-authored-by: multica-agent <github@multica.ai>
* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)
Two follow-ups from MUL-2662 review round 2:
- CreateProject inline resources path now dedupes local_directory entries on
(daemon_id, local_path) before opening the transaction. The DB-level
UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
full JSON match, so two rows with the same target but different `label`
would otherwise slip past. Standalone POST/PUT already cover this via
findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
row before applying per-type shortcut flags, so `--default-branch-hint x`
on its own no longer constructs a payload missing `url` (which the server
400s on). Local_directory partial edits get the same merge behavior.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)
* feat(desktop): local_directory project_resource UI (MUL-2665)
First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.
What's new for the renderer:
- ProjectResourcesSection grows a desktop-only "Add local directory"
button next to the existing GitHub-repo popover. Clicking it opens
Electron's native folder picker, validates the path through a new
IPC pair (existence + r/w), and submits a project_resource of
resource_type=local_directory with daemon_id pulled live from
daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
greys out when ref.daemon_id != this machine's daemon_id (with a
"only available on the machine that registered this directory"
tooltip). Delete stays enabled so users can drop stale registrations
from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
shows "Agent will work in-place at {label} ({path})" when the issue's
project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
that the daemon will publish when it dequeues a task but can't
acquire the path lock. The render is in place now so the daemon
sibling subtask can wire the status string without an additional UI
PR.
Plumbing:
- @multica/core/types gains LocalDirectoryResourceRef +
UpdateProjectResourceRequest, and the api client gets the matching
PUT method backed by the server endpoint that landed in
|
||
|
|
13f74e651a |
feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600) (#3209)
* feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600)
The agent resource shape (list / get / create / update / archive /
restore responses + WebSocket events) no longer carries `custom_env`
values. Reads/writes of env now flow exclusively through a dedicated
`/api/agents/{id}/env` endpoint that is owner/admin-only, rejects
agent-actor sessions, applies a "****" sentinel preserve guard on
PUT, and writes a persistent audit row per reveal/update.
Why
- `multica agent list --output json` historically returned plaintext
`custom_env` for owner/admin callers (the redaction gate gave only
members the masked map). Any agent token running on the workspace
inherits its owner's role and could read every other agent's
secrets just by listing.
- Patching list/get redaction alone (PR #3175 direction) left
symmetric leaks via mutation responses, WS events, the "reveal"
path itself (no actor-aware auth), and a `****` overwrite footgun
on UpdateAgent.
What changed
- Backend: drop `custom_env` from AgentResponse; add coarse
`has_custom_env` + `custom_env_key_count`. Strip env handling from
UpdateAgent (silently ignored if sent). Keep CreateAgent's
custom_env acceptance.
- Backend: new GET/PUT `/api/agents/{id}/env` handlers in
`internal/handler/agent_env.go`:
- resolveActor → 403 for agent actors (closes the lateral-movement
path).
- Owner/admin role gate via existing helper.
- PUT honours value == "****" as "preserve existing value".
- Both write to `activity_log` with `agent_env_revealed` /
`agent_env_updated` actions. Audit details record key names only,
never values.
- Daemon claim path (`ClaimAgentTask`) unchanged — `TaskAgentData`
still carries plaintext env for runtime injection.
- SQL: new `UpdateAgentCustomEnv` query; sqlc regenerated (v1.31.1).
- CLI: new `multica agent env get|set` subcommands. `--custom-env*`
flags removed from `multica agent update`; the no-fields error
now points to the new path.
- Frontend: drop env fields from `Agent` + `UpdateAgentRequest`; add
`getAgentEnv` / `updateAgentEnv` client methods; rewrite env-tab
to show "N variables configured" + explicit "Reveal & edit"
button, fetching values only on intentional reveal.
- Locales: parity-safe additions to en + zh-Hans.
- Docs: agents-create.{mdx,zh.mdx} reflect the new threat model and
endpoint.
- Mobile: schema drops `custom_env` / `custom_env_redacted`, adds
metadata fields.
Tests
- Handler tests pinned the new invariants: no env in list/get
responses, owner reveal happy-path + audit row, agent-actor 403,
`****` sentinel preserves real values, UpdateAgent silently
ignores `custom_env`, pure `mergeAgentEnv` cases.
- CLI tests pivot to the new flag surface: `agent update` MUST NOT
expose the env flags; `agent env set` MUST expose
--custom-env-stdin/--custom-env-file.
- Frontend test fixtures updated; pnpm typecheck / test / lint
pass cleanly.
This is a breaking API change. Scripts that read `custom_env` from
`/api/agents` must migrate to `GET /api/agents/{id}/env`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close actor-spoofing + audit fail-closed in env endpoints (MUL-2600)
Addresses Elon's review of #3209:
* Mint a task-scoped `mat_` token per claim, bound to (agent, task,
workspace, owner). Daemon injects it into the agent process in place
of its own credential. Auth middleware authoritatively rebuilds
X-User-ID / X-Agent-ID / X-Task-ID from the token row and sets
X-Actor-Source=task_token; that header is server-set only — incoming
values are stripped before any auth branch runs. resolveActor honors
the header so an agent that strips X-Agent-ID / X-Task-ID still
resolves as actor=agent.
* GetAgentEnv / UpdateAgentEnv are now fail-closed on audit-log
failures: GET refuses to return plaintext, PUT persists inside the
same tx as the audit row so they commit/roll back together.
* PUT /api/agents/{id} returns 400 when the body carries custom_env
instead of silently dropping it — directs callers to the audited env
endpoint.
* Agent actors never see mcp_config, even when the underlying member
is owner/admin; mutation broadcasts go through a redaction shim so
WS subscribers don't pick it up either.
* Fix backend test that asserted dense JSON (jsonb::text renders
whitespace) and frontend test that assumed a unique "Test User"
match.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close residual MUL-2600 gaps from review (MUL-2600)
Migration 108 FK now correctly references agent_task_queue(id) instead
of the non-existent agent_task table; the previous name blocked CI
backend migrations.
Task-token-authenticated requests can no longer be re-routed at a
different workspace by passing workspace_slug / workspace_id /
?workspace_id / a URL workspace param. ResolveWorkspaceIDFromRequest
and resolveWorkspaceUUID both short-circuit on X-Actor-Source=task_token
and return only the token-bound X-Workspace-ID; buildMiddleware adds a
defence-in-depth 403 if any URL-resolved workspace disagrees with the
token binding.
mcp_config no longer leaks back to agent actors through UpdateAgent /
CreateAgent / ArchiveAgent / RestoreAgent HTTP responses — the same
redactAgentResponseForActor helper that GetAgent/ListAgents use is now
applied to mutation responses too. WS broadcasts were already redacted
via broadcastAgentResponse.
FailTask and every TaskService cancel path (CancelTask /
CancelTasksForIssue / CancelTasksForAgent / CancelTasksByTriggerComment
/ BroadcastCancelledTasks) now eagerly DeleteTaskTokensByTask so the
mat_ token's 24h window doesn't outlive a terminated task. Failure is
non-fatal — the FK cascade and expiry remain durable guards.
Doc-only: clarify that PUT /api/agents/{id} now hard-rejects bodies
that carry custom_env (was previously "silently ignores").
Tests:
- middleware: TestResolveWorkspaceIDFromRequest gains a task_token
case asserting client-supplied slug/id/query cannot override the
bound workspace.
- handler: TestUpdateAgent_RedactsMcpConfigForAgentActor and
TestUpdateAgent_KeepsMcpConfigForMemberActor pin the mutation-
response redaction contract per actor type.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): match redacted mcp_config as JSON null, not Go nil (MUL-2600)
`AgentResponse.McpConfig` is `json.RawMessage` without `omitempty`, so
the redacted response serialises as `"mcp_config": null`. On decode,
`json.RawMessage` keeps the literal bytes `null` rather than collapsing
to Go nil, which made the assertion fire on a non-leak.
The product contract (field always present, distinguished from "no
config" via `mcp_config_redacted`) is intentional, so adjust the test
to check for "no secret-bearing content" instead of weakening the
contract via `omitempty`.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
3e1066a638 |
feat(ui): add repository search to resource pickers (#3126)
* feat(ui): make project resource list scrollable * feat(ui): add repository search to resource pickers |
||
|
|
f2e6dc75bd |
feat(create-issue): collapse start date into ⋯ overflow menu (#3063)
Start date is a low-frequency field for most issues, so the always-on inline pill was crowding the property toolbar. Move it behind the ⋯ overflow menu by default: the pill only appears once a value is set, or transiently while the calendar popover is open after the user picks "Set start date..." from the menu. Closing the popover without a value returns the pill to the menu-only state. To make the menu item open the popover programmatically, lift the picker's open state via new controlled `open` / `onOpenChange` props (matching the priority-picker pattern). MUL-2557 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7e55813460 |
fix(ui): show tooltip when create-issue button is disabled due to empty title (#2943)
Co-Authored-By: Xiaomi MiMo V2.5 Pro |
||
|
|
933f417dac |
fix(views): clear manual draft when packing into agent prompt (#2370)
When alternately switching between manual and agent modes in the create-issue dialog, the title and description were being duplicated and accumulated on every round-trip. Root cause: manual→agent packed title+description into the agent prompt but left them in the shared useIssueDraftStore; the subsequent agent→manual wrote the agent markdown into draft.description while the stale draft.title persisted, so the remounted manual panel surfaced both. Clear title/description from the shared draft at the moment they move into the agent representation, so round-trips can't layer stale manual state on top of prompt-as-description. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
20c2f45b4a |
fix(views): surface backend error messages on mutation failures (MUL-2317) (#2772)
* fix(views): surface backend error messages on mutation failures (MUL-2317)
Mutation toasts across the views package were swallowing the backend
`error` string and showing only a generic i18n fallback. This made it
impossible for users to see why an operation failed (most visibly:
creating an issue with a duplicate title produced a vague "Failed to
create issue" toast).
The fix has three pieces:
1. Create-issue duplicate branch (A段)
- New schema `DuplicateIssueErrorBodySchema` in core/api/schemas.ts.
- `create-issue.tsx` parses `ApiError.body` via `parseWithFallback`
and renders a dedicated amber-toned toast with a "view existing"
link when the server returns `{ code: "active_duplicate_issue",
issue: {...} }`. Schema drift downgrades to the normal error toast.
- Schema intentionally omits `issue.status` so the toast does not
depend on `StatusIcon`, which has no fallback for unknown enums.
2. User-facing mutation failure toasts (B段)
- 47 sites converted to `err instanceof Error && err.message ?
err.message : <existing fallback>` — preserves all existing
code-specific branches (slug conflict, agent_unavailable,
daemon_version_unsupported) and i18n keys.
- Covers Type 1 (onError) and Type 2 (catch block) patterns across
issues, projects, autopilots, inbox, runtimes, squads, comments,
batch actions, workspace create, and agent config tabs.
3. Autopilot partial-success (Type 3)
- New i18n keys `toast_create_partial_with_reason` /
`toast_update_partial_with_reason` (double-brace `{{reason}}`).
- `autopilot-dialog.tsx` captures `err.message` in the schedule
`catch` and routes to the `_with_reason` variant when present,
preserving the partial-success semantic (autopilot saved, schedule
failed) while exposing the actual reason.
Explicitly out of scope:
- `packages/core/` mutation hooks (no global onError, no UI dependency)
- No `toastApiError` helper (matches existing 14+ correct sites)
- Sub-issue link aggregate `Promise.allSettled` keeps count-based toast
(N independent requests cannot collapse to one err.message); only
added a dev-side `console.error` per rejection.
- Clipboard catches and `useUpdateChatSession` (not API mutation toasts)
Tests:
- `packages/core/api/schemas.test.ts` — schema contract (valid body,
forward-compat fields, rename rejection, missing issue, wrong types).
- `packages/views/modals/create-issue.test.tsx` — duplicate toast +
view link, schema-drift fallback, err.message surfacing, non-Error
fallback (4 new cases).
- `packages/views/autopilots/components/autopilot-dialog-i18n.test.ts`
— real i18next, asserts rendered text contains the reason verbatim
(guards against `{reason}` vs `{{reason}}` regression).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): unify rotate-token catch + cover dialog partial-success render
Address reviewer feedback on PR #2772:
1. webhook-token rotate (`autopilot-detail-page.tsx`) now follows the
`err.message ?? fallback` ternary used by the sibling trigger
delete/add paths, instead of swallowing the error.
2. Extract `formatSchedulePartialFailureToast` so the dialog's
partial-success branches and the i18n test exercise the same
helper. The test now drives the actual format function, so a
variable-name typo at the call site (e.g. `{ msg }` instead of
`{ reason }`) fails the substring assertion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(modals): drop user.type for title in success path to dodge CI 5s timeout
The success-path test typed the 42-character title via userEvent which
triggers a controlled re-render per keystroke. On the slower CI runner
the whole test crept up to ~5s and intermittently tripped the default
vitest timeout. Setting the value in one shot via fireEvent.change cuts
the cost while leaving the submit + toast interactions on userEvent.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
3645bdb5b6 |
feat(issues): add start_date field with progressive disclosure (MUL-2274) (#2696)
* feat(issues): add start_date field with progressive disclosure (MUL-2274) Mirrors the existing due_date implementation end-to-end so an issue can express a planned start in addition to a deadline. Surfaces start_date as an optional sidebar property alongside priority / due_date / labels (added in MUL-2275), with consistent picker, board/list/sort, activity, and inbox plumbing. Backs the Project Gantt work (parent MUL-1881) and keeps the progressive-disclosure attribute experience consistent. - DB: migration 091 adds issue.start_date TIMESTAMPTZ. - sqlc: ListIssues / CreateIssue / UpdateIssue / CreateIssueWithOrigin / ListOpenIssues read & write start_date. - Backend: IssueResponse + create/update/batch-update handlers parse and emit start_date with RFC3339 validation; new start_date_changed activity event + subscriber notification (with prev_start_date in event payload). - CLI: --start-date flag on `multica issue create` / `issue update`. - Frontend: StartDatePicker component, start_date wired into Issue type, Zod schema, draft / view stores, sort util, header sort + card-property options, list-row / board-card display, create-issue modal, and the issue-detail progressive-disclosure "+ Add property" surface (visibility rule, picker row, add-property menu icon + label). - i18n: en + zh-Hans for sort_start_date / card_start_date / prop_start_date / activity start_date_set / start_date_removed / picker start_date.trigger_label / clear_action / inbox labels. - Tests: new TestNotification_StartDateChanged; existing Issue / draft / modal fixtures extended with start_date. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): align start_date with due_date in actions menu and CLI table - Add Start Date submenu (today / tomorrow / next week / clear) in actions menu, mirroring Due Date — parity with the Due Date quick setters in list/board context and 3-dot menus. - Add corresponding en / zh-Hans i18n keys (actions.start_date / start_today / start_tomorrow / start_next_week / start_clear). - CLI human table for `multica issue list` and `multica issue get` now shows a START DATE column next to DUE DATE; --full-id variant too. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0079a73430 |
fix(views): narrow agent/squad create dialogs to max-w-2xl (#2706)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e88156356 | Add assignee grouping for issue boards (#2693) |