* fix(labels): sweep resource-label junctions on bulk deletes (MUL-4479)
Runtime, runtime-profile, and workspace deletion hard-delete their agents
and skills without clearing agent_to_label / skill_to_label. Migration 173
dropped the junction foreign keys, so these rows are no longer cascade-cleaned;
once resource labels are enabled, every labelled agent/skill removed through one
of these bulk paths leaves a permanent, invisible orphan junction row.
Clear the junctions in the same transaction as the owner delete, before the
owning rows disappear:
- DeleteAgentRuntime / ArchiveAgentsAndDeleteRuntime / DeleteRuntimeProfile:
DeleteAgentLabelAssignmentsByRuntime, ahead of the archived-agent hard-delete.
- DeleteWorkspace: DeleteAgentLabelAssignmentsByWorkspace +
DeleteSkillLabelAssignmentsByWorkspace, ahead of the workspace cascade.
Adds regression tests for all four delete paths.
Follow-up to #5345 (Elon review). Resource labels must stay disabled until this
lands.
Co-authored-by: multica-agent <github@multica.ai>
* fix(labels): make workspace cleanup atomic
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Eve <eve@multica-ai.local>
The 162_resource_labels down migration recreated issue_label_workspace_name_lower_idx
with a plain CREATE UNIQUE INDEX, which takes a blocking lock on the existing
issue_label table during a rollback to v0.3.43 and violates the online-migration
rule that every CREATE INDEX must be CONCURRENTLY.
CREATE INDEX CONCURRENTLY cannot run in a transaction or a multi-command migration,
so the rebuild is split out of 162.down:
- 171.down now rebuilds the legacy index as a single CREATE UNIQUE INDEX CONCURRENTLY
(the natural inverse of 171.up, which drops it).
- 174 (new, no-op up) deletes the agent/skill label rows in its down so they are gone
before 171.down rebuilds the workspace-wide unique index. Down migrations apply
high->low, so 174.down -> 171.down -> 162.down runs in the required order.
- 162.down keeps only the transaction-safe structural teardown.
Validated on an isolated Postgres: full up->down->up chain with a colliding
issue/agent label pair; the concurrent rebuild succeeds after the rows are deleted,
restores the valid pre-162 unique index, and a negative control (rebuild before the
delete) fails on the exact collision.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Drop the "使用模板" card and its onTemplate wiring from ModeChooser so the
agent creation studio only offers blank and AI modes. Rebalance the mode
grid to two columns. TemplateChooser and the template creation path are
left in place but no longer reachable from the UI (known follow-up debt).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* 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>
* fix(codex): bound app-server startup RPCs
Co-authored-by: multica-agent <github@multica.ai>
* test(codex): de-flake bounded-handshake test
The single 500ms handshake bound was shared by the successful preamble
RPCs, so a slow fork/exec of the /bin/sh fake app-server could make
initialize spuriously time out under parallel load. Raise the test bound
to 3s (still below the 5s semantic timeout and 10s harness ceiling) and
loosen the elapsed assertion to match.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
Moving an issue to `cancelled` used to auto-cancel every in-flight agent task
on that issue. Users have no expectation that clicking "cancel" stops running
agent runs, so this implicit coupling is removed from UpdateIssue and
BatchUpdateIssues. Deleting an issue still cancels its tasks (the owning row
disappears); a plain status change never does. Reassignment already didn't
cancel tasks (#4963 / MUL-4113), so this makes status-cancel consistent.
- Status-table-driven regression tests cover every active state the cancel
query sweeps (queued / dispatched / running / waiting_local_directory /
deferred) on both the single and batch paths.
- Updated the multica-working-on-issues skill (SKILL.md + source map) and
corrected stale comments in task.go and agent.sql that described the removed
coupling as current.
MUL-4465
* feat(shortcuts): make browser-reserved accelerators recordable on desktop
Cmd/Ctrl+P (and L/T/N/D/U) were rejected by the shortcut recorder on every
platform because a browser tab cannot reliably own them. The Electron
renderer receives these as plain keydowns — neither Electron's default menu
nor the desktop shell binds any of them — so the reservation now only
applies to the web runtime.
Adds a ShortcutRuntime dimension (configured by CoreProvider from the
client identity, with a preload-global fallback that is already correct at
module-eval time so store hydration sanitizes with the right runtime).
App-owned accelerators (W/R/Q, editing keys, zoom row) stay reserved
everywhere.
Closes MUL-4457
Co-authored-by: multica-agent <github@multica.ai>
* fix(shortcuts): limit desktop unlock to bare primary browser accelerators
Review follow-up (MUL-4457): the desktop skip matched primary+key with any
extra modifiers, which would also unreserve OS-owned combos such as
Option+Cmd+D (macOS Dock toggle) and Ctrl+Alt+T (Linux terminal). Only the
bare primary chord is now recordable on desktop; every extra-modifier
variant keeps the historical reservation on both runtimes. Adds regression
tests for the OS combos.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(skills): search runtime local skills
* feat(skills): highlight matched substrings in runtime local skill search
Reuse the shared HighlightText (the same component the global search
command uses) to highlight matched substrings in a result's name,
provider, description, and path, so styling stays consistent across the
app. Narrow the search to the fields the row actually renders and drop
`key`, so every match maps to something visible. While a query is active,
lift the description's 2-line clamp so a match past the first two lines
stays on screen instead of being clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): zero unread for archived chat sessions across all badges (MUL-4360)
Archiving a chat session flips status but deliberately does not advance
last_read_at, and ListAllChatSessionsByCreator counted unread
unconditionally. So an archived session that had unread replies kept
reporting has_unread=true / unread_count>0 — a stuck badge the user can
never clear (archived sessions are read-only and hidden from history, so
there is no mark-as-read entry). MUL-4372 fixed only the quick-chat FAB
surface; the sidebar Chat tab badge and the chat-window "other unread"
header still counted it.
Fix at the source: derive unread_count = 0 for status='archived' rows in
ListAllChatSessionsByCreator. Because has_unread is server-derived as
unread_count > 0, and all surfaces (FAB, sidebar via
countUnreadChatMessages, chat-window header, and mobile) read this one
payload, every badge drops archived sessions with no per-surface filter.
last_read_at is left untouched so unarchiving restores the true unread
state. Installed desktop clients benefit without an app update.
Also zero unread optimistically in the archive mutation so no badge
counts a just-archived session in the frame before the refetch lands
(FAB already filtered archived; this keeps sidebar/header consistent).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): stop auto-mark-read from clearing a transiently-active session on mount (MUL-4360)
The chat page persists `activeSessionId`, so on a bare `/chat` navigation it
restores the last-open session as active for one frame before its URL→store
effect (which runs AFTER useChatController's effects, since the hook is called
first) clears it back to null. The auto-mark-read effect fired in that gap and
marked the restored-but-never-opened session read — its unread badge vanished
though the right pane still showed "select a chat" and the user never opened it.
This is why the sidebar count dropped (e.g. 2 → 1) just by entering the tab.
Defer the read by a tick and cancel it on cleanup: a session that is only
momentarily active (restored on mount, then cleared) has its pending read
cancelled when activeSessionId changes; only a session that stays active past
the tick — a real select, deep link, or refresh — is marked read. A live-store
re-check in the timer is a belt-and-suspenders guard.
Adds the previously-missing auto-mark-read coverage: a stable-active session is
read after the tick; a momentarily-active one is not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: coderbaozi <YHbaozi1988@163.com>
Co-authored-by: abun <103836393+coderbaozi@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The settings nav and agent overview aside tinted themselves with
bg-app-shell/70, pulling the window-chrome tone inside the content
card. Since the desktop's active tab merges into the card top
(MUL-4439), a tinted panel under the first tabs visibly broke the
tab/content seam. Zone separation stays with the hairline divider,
row hover/selected states, and the narrow column — matching the
inbox panel's existing pattern.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
#5307 renamed 161_attachment_task_id -> 164 (and 162_..._index -> 165) to
resolve a prefix collision. schema_migrations keys on the full stem, so any DB
that applied the migration under its old 161 number does not have "164" in the
ledger and the runner re-applies the renamed file. The bare `ADD COLUMN task_id`
then aborts with 42701 ("column already exists"), blocking every later
migration — this is exactly what crashed the dev deploy of main (bf288349f) at
container startup, before it ever reached 166_project_dates.
Add `IF NOT EXISTS` so the re-run is a harmless no-op on already-migrated DBs
(dev/staging/prod) with no manual schema_migrations surgery, while staying
identical on a fresh DB. Sibling 165 already uses CREATE INDEX ... IF NOT EXISTS
for the same reason; this brings 164 in line. The down file already uses
DROP COLUMN IF EXISTS.
Verified on a throwaway DB reproducing the dev drift (forget 164/165 in the
ledger, keep the column+index): old file reproduces the 42701 crash, the fixed
file self-heals and completes. Migration lint + concurrent migrate tests pass.
Refs #5307
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The active tab now shares the content card's fill and keyline: rounded
top corners, concave bottom flares (radial-gradient corner pieces whose
1px arc hands the tab border over to the card's top ring), and a
borderless base that runs into the card so the two read as one surface.
Inactive tabs sit flat on the shell with an inset hover pill and
hairline separators that hide around the active tab, Chrome-style.
Closes MUL-4439
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Projects become schedulable planning objects alongside their issues: add
optional start_date / due_date, mirroring issue.start_date / issue.due_date.
This is only the first slice of #5227 — labels, metadata, and the editable
metadata UI are still out of scope.
- migration 166: two nullable DATE columns on `project` (calendar days, no
FK/index — matches the issue end-state after migration 112)
- sqlc CreateProject / UpdateProject carry the dates; UpdateProject uses
narg so an explicit null clears
- handler: parse YYYY-MM-DD (400 on bad format), rawFields-presence clear on
update, and the hand-scanned SearchProjects query returns the columns
- CLI: `project create/update --start-date/--due-date` (empty clears on update)
- frontend + mobile types/zod schemas: the two new schema fields are
nullable().default(null) so a project from an older backend (frontend
deploys before backend) parses to null instead of degrading the batch to
the empty fallback; added a search schema drift test
- projects skill / CLI docs
Part of #5227
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(skills): search runtime local skills
* feat(skills): highlight matched substrings in runtime local skill search
Reuse the shared HighlightText (the same component the global search
command uses) to highlight matched substrings in a result's name,
provider, description, and path, so styling stays consistent across the
app. Narrow the search to the fields the row actually renders and drop
`key`, so every match maps to something visible. While a query is active,
lift the description's 2-line clamp so a match past the first two lines
stays on screen instead of being clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GitHub-token rule only matched classic tokens (ghp_/gho_/ghu_/ghs_/ghr_),
even though its comment claims to cover fine-grained tokens. Add coverage for
GitHub fine-grained PATs (github_pat_), Slack app-level (xapp-) / config (xoxe-),
Google API keys (AIza...), and Stripe live secret/restricted keys (sk_live_/rk_live_).
Publishable Stripe keys (pk_live_) are intentionally NOT redacted (public).
Adds regression tests for every new shape, including a positive test that
pk_live_ stays unredacted.
`161_attachment_task_id` and `162_attachment_task_id_index` (from #5164)
merged after prefixes 161/162/163 were already taken on main by #5277,
#5279, and #5296, leaving two migrations on each of 161 and 162. That trips
TestMigrationNumericPrefixesStayUniqueAfterLegacySet, so the backend job now
fails on every open PR. Renumber to the next free prefixes (164/165); the
file contents are unchanged.
Note for already-migrated databases: any DB that applied 161/162 has those
versions recorded in schema_migrations. After this rename, reconcile the
ledger (rename the recorded versions to 164/165) or reset the dev DB —
otherwise the migrator re-runs the renamed files and the ADD COLUMN fails
because the column already exists.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Archiving a channel-bound chat session now severs its
channel_chat_session_binding in the same tx as the status flip. The web
send path already treats status='archived' as read-only, but the channel
engine (Feishu/Slack) resolves inbound traffic through the binding without
checking session status, so an archived-but-still-bound session kept
receiving agent replies and a stuck, uncleared unread badge. Dropping the
binding makes the next inbound message fork a fresh session; unarchive does
NOT recreate it (a later session may already own the channel).
The FAB unread badge counted status=all sessions without excluding
archived, so residual unread on an archived session (archive does not
mark-read) held the badge even though the session is hidden and read-only.
Extract countUnreadChatSessions() and exclude archived. This matches what
mobile already computes (active-only list), restoring count parity.
Tests: backend archive-clears-binding + unarchive-does-not-recreate;
frontend countUnreadChatSessions archived-exclusion cases.
MUL-4372
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): support images/files in agent chat replies (MUL-4287)
Agents can now attach images/files to their chat replies, matching how
comment attachments already work. The write-side gap was that the assistant
chat_message is synthesized server-side from the completion callback's text
output and never bound any attachments.
Backend:
- migration 150: nullable attachment.task_id (+ partial index), the transient
handle that ties an agent's in-run upload to the reply it produces.
- POST /api/upload-file accepts task_id: gated to the task's own agent, in
this workspace, on a chat task; tags the row with task_id + chat_session_id.
- CompleteTask (chat branch) binds the task's still-unclaimed attachments to
the assistant message via BindChatAttachmentsToMessage (rejects rows already
owned by an issue/comment/chat_message). An empty-output reply that produced
files still creates a message so the images have an owner. FailTask binds
nothing.
CLI:
- `multica attachment upload <path>` uploads a file for the current chat task
(task from MULTICA_TASK_ID or --task) and prints id / markdown_url / a
ready-to-paste markdown snippet.
Prompt:
- web/mobile chat prompt tells the agent how to attach a file to its reply.
Mobile:
- chat:done handler now always invalidates the messages list so attachments
(absent from the event payload) refetch; mirrors web's self-heal.
- chat bubbles render standalone attachment cards via the existing
CommentAttachmentList (dedup vs inline references), matching web.
Web/desktop needed no change — they already render message.attachments inline
and via AttachmentList, and self-heal on chat:done.
Tests: upload permission/isolation, bind-on-complete, empty-output+attachments,
FailTask no-bind, null task_id untouched, already-owned not stolen, CLI output
contract, mobile refetch-on-done.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): address review blockers on chat reply attachments (MUL-4287)
Two final-review blockers on PR #5164:
1. Mobile inline dedup only checked raw `url`, so an attachment referenced
inline via `markdown_url` (exactly what the CLI snippet emits) rendered
twice — once inline, once as a standalone card. Reuse the core
`contentReferencesAttachment` helper so dedup covers every real reference
form (stable /api/attachments/<id>/download path, url, download_url,
markdown_url), matching web's AttachmentList. Extracted the filter into a
pure `lib/attachment-dedup.ts` so it is unit-testable, and added a
regression test covering `content` containing `attachment.markdown_url`
(plus the other URL forms and same-identity sibling dedup).
2. CLI `attachment upload` emitted `![...]` image markdown for every file,
producing a broken-image snippet for non-images. Emit image markdown only
for image/* content types and a plain link otherwise, with a CLI contract
test for both.
Approved scope otherwise unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): renumber attachment_task_id migration 150 -> 157 after main merge (MUL-4287)
Merged latest main; main renumbered its migrations and now occupies 150-156,
so 150_attachment_task_id collided with 150_agent_task_coalesced_comments and
would fail TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Renamed to the
next unique prefix (157). No content change; migrate up applies cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): render agent-produced files as attachment cards, not raw links
The chat upload command handed the agent a bare `[name](url)` markdown
snippet. Pasted mid-sentence it renders as a plain text link (not a card),
and the referenced URL hides the auto-bound standalone attachment — so a
file the agent produced could end up showing as nothing.
Return the block-level `!file[name](url)` card syntax instead (images keep
`` inline), and markdown-escape the filename so names with `[`/`]`
don't truncate the label. The prompt and CLI help now state the file
auto-attaches below the reply and the snippet is optional, only for placement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): soften message-list scroll fade (32px → 16px)
The 32px edge fade washed out full-bleed content (HTML / image previews)
at the list edges. Halve the fade distance so it barely grazes previews
while still hinting at more content above/below.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): renumber attachment_task_id migration 157 -> 158
main landed 157_agent_task_delivered_comments while this branch was open,
colliding on prefix 157 and failing TestMigrationNumericPrefixesStayUniqueAfterLegacySet.
Bump this PR's migration to the next free prefix (158). Rename only; the
migration body (nullable attachment.task_id + partial index) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): pin attachment upload to the token's task; build index concurrently
Two code-review findings on the chat-attachment path (MUL-4287):
- Isolation/privacy: POST /api/upload-file only checked the form task_id
belonged to the caller's agent, not that it matched the task-scoped token's
authoritative X-Task-ID. A run authorized for task A could tag an attachment
onto task B (another chat task of the same agent, possibly another user's
session), binding it into that reply on completion. Require the form task_id
to equal the server-set X-Task-ID; add a same-agent/other-task 403 regression.
- Migration: split the task_id lookup index into its own migration (159) built
with CREATE INDEX CONCURRENTLY (repo convention) — it cannot share a
multi-command file with the ADD COLUMN in 158.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): enforce task-token source on attachment upload; drop transient task_id FK (MUL-4287)
Addresses the two remaining Preflight BLOCKERs on PR #5164.
Security (file.go): the task_id upload path compared the form task_id to
X-Task-ID but did not require X-Actor-Source=task_token. A normal JWT/mul_ PAT
leaves that header empty and the middleware does NOT strip a client-forged
X-Task-ID; resolveActor's fallback accepts a valid X-Agent-ID+X-Task-ID pair.
So a member who learned a task ID could forge both and inject an attachment
onto another chat task's assistant reply (cross-session/privacy leak). Now the
branch requires X-Actor-Source=task_token first (mirrors chat_history.go's
load-bearing boundary), then pins to the middleware-injected X-Task-ID. Tests
now go through the real task-token headers and add a forged-JWT-403 regression.
Migration (158): task_id is a transient binding handle (written once at upload
against an already-validated task, read only during that task's own
completion; durable owner is chat_message_id). There is no app-layer path that
hard-deletes agent_task_queue rows, and orphan uploads are already reaped by
attachment.chat_session_id's ON DELETE CASCADE — so an FK here would only add a
cascade dependency the app never relies on plus write overhead on the hot
attachment table. Drop the FK; task_id is now a plain UUID column. Added a
regression test that an unbound task-tagged upload is reaped on chat_session
delete. Index (159, CONCURRENTLY) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(mobile): align !file card preprocess with web parser + CLI escaped labels (MUL-4287)
Howard final-review blocker: mobile's `!file[...]` preprocess didn't keep up
with the CLI's file-card output, so agent-produced non-image files rendered
nowhere on mobile.
- `FILE_LINE_RE` used `[^\]]+` for the label, so the CLI's escaped-bracket
output `!file[a\]b.pdf](url)` (cmd_attachment.go escapeMarkdownLabel) never
matched — the line stayed literal AND `standaloneAttachments` still hid the
fallback card (the URL is in `content`), so the file showed nowhere.
- Align the matcher with web's `packages/ui/markdown/file-cards.ts`: label
allows backslash-escaped metacharacters (ReDoS-safe class), and the URL is
restricted to the same allowlist (site-relative /uploads + /api/attachments/
<UUID>/download, plus absolute http(s)); disallowed schemes stay plain text.
- Unescape the label to the real filename, then re-escape only the chars that
would break a markdown LINK label (mobile emits `[📎 name](url)`, re-parsed
by the renderer — unlike web's HTML data-filename), so a raw `]` never
truncates the link text.
No dedup change: once the inline `!file` renders, hiding the standalone card is
correct. Added focused unit tests covering the escaped-label case, parens/
backslash unescape, the site-relative URL form, and disallowed-scheme rejection.
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>
* feat(issues): add sticky setting for the issue comment bar (MUL-4435)
The bottom comment composer can now pin itself to the scroll viewport so
it stays reachable while reading a long timeline. A pin toggle on the bar
itself persists the preference (default: on) via a new localStorage-backed
useCommentComposerStore. While pinned, the editor area is height-capped so
long drafts scroll internally instead of covering the timeline.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(settings): move the sticky comment bar toggle to Settings > Preferences (MUL-4435)
Per review feedback: pinning the comment bar is a low-frequency choice, so
the toggle moves off the bar itself into Settings > Preferences as a Switch
row. The composer keeps reading the store for the sticky behavior and the
40vh editor cap; the pin button and its issues-locale tooltips are removed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): clarify 409 when a comment-triggered task posts a top-level comment (MUL-4417)
A comment-triggered task that posted a parentless top-level comment on its
own issue got a 409 whose message named the required parent id but never
said top-level comments are disallowed. Agents misread it as the issue being
locked and deleted good replies trying to reset. Keep the guard (agents must
reply under their trigger comment), but make the error self-explanatory and
document the constraint in the CLI --parent help. Add handler-level tests
pinning the rejected top-level case and the allowed reply-under-trigger case.
Refs GH #5266.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): tighten 409 wording and assert the fix hint (MUL-4417)
Review nits on #5292: drop the inaccurate "while it is active" phrasing and
the redundancy from the 409 message so it matches the actual allow-set
(trigger or coalesced comment); collapse the incident narration to one line;
and assert the actionable parent_id (--parent) hint in the regression test so
the guidance can't be dropped silently.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J (Multica agent) <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): add DM button on agent detail page
The header gains a DM action next to Assign work. It shares the MUL-3963
invocation gate with assignment: allowed users land on the Chat tab with a
fresh compose bound to the agent via a new one-shot ?agent=<id> deep link
(consumed once the permission-filtered agent list resolves, then stripped);
denied users get an explanatory toast instead of a hidden affordance.
Closes MUL-4432
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): harden the ?agent= deep link against races and undetermined permissions
Review follow-up for the DM button (PR #5289):
- An explicit user action (thread select, archive, manual new chat) now
supersedes a still-pending ?agent= intent, so an intent deferred by slow
agent/member queries can no longer fire late and clobber that choice.
- The composingNew reset reads the live store value; under StrictMode's
effect replay the render-captured snapshot re-closed the compose pane the
intent had just opened (one-shot guard rightly refuses to re-fire).
- A settled miss (revoked access, archived agent, bad id) now toasts and
strips the param instead of silently keeping a re-fireable intent; still-
loading queries keep the intent pending. Query errors never settle.
- useAgentPermissions exposes isLoading: the detail page disables DM while
membership resolves instead of toasting a false not_member deny at a
legitimate member.
The chat store test mock is now reactive (useSyncExternalStore) — with a
plain mutable ref React bails out of committing post-effect re-renders and
the StrictMode regression cannot reproduce. Both new regression tests fail
against the pre-fix code.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Replace per-row controlClassName widths with a size enum (text /
select-wide / select / code / none) on SettingsRow. Text-entry fields
(name, slug, description, context, bio) now share the text tier so
their control edges align within a card; deliberately-short fields
(issue prefix) keep a clearly smaller tier.
MUL-4428
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Detect the bundled Codex CLI under the relocated ChatGPT.app on macOS, while keeping the legacy Codex.app path so older installs still resolve.
Closes#5205
Fix garbled `multica login -h` output: the --token line printed a raw NUL and a hijacked value placeholder. Change the NoOptDefVal sentinel to a printable value and drop backticks from the usage string. Add a regression test that renders the flag help through pflag's real path and asserts no control bytes plus the standard --token string[="prompt"] form.
Co-authored-by: YYClaw <197375+yyclaw@users.noreply.github.com>
Co-authored-by: J <j@multica.ai>
SubmitButton is now always circular — the shape prop is removed since
every composer uses the same silhouette. ReplyInput drops its inline
icon-xs Button for the shared SubmitButton, gaining the same size,
disabled state, and send tooltip as the comment composer and chat.
MUL-4433
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): let workspace members view runtime capabilities (MUL-4427)
The Agent capabilities redesign (#5277) reused the runtime local-skills
discovery endpoint on Agent detail surfaces, but the endpoint kept the
owner-only gate from the original import flow. Viewing an agent bound to
someone else's runtime returned 403, which the Skills / MCP tabs rendered
as 'try again when the runtime is online' even though the runtime was
online.
- Discovery (list + poll) now requires workspace membership only; the
payload is the deliberately redacted inventory built for this display.
- Import (init + poll) stays owner-only: it copies skill file contents
off the owner's machine.
- The failed notice no longer blames runtime connectivity, and a 403
(new client against an older backend) gets an honest permission
message.
* test(settings): stub Intl.supportedValuesOf in timezone picker tests
The preferences-tab timezone tests drove a ~600-option Base UI Select
through userEvent in jsdom; on slow CI runners the clear-preference case
exceeded even its extended 20s per-test timeout (PR #5281 frontend job).
Stub the IANA enumeration down to the curated COMMON_TIMEZONES fallback
— everything the tests pick lives there too — and drop the now-unneeded
20s overrides. File test time drops from ~35s to under 1s.
---------
Co-authored-by: Lambda <lambda@multica.ai>
The agent detail page wrapped both the sidebar rail and the content pane
in one scroll container, so scrolling any Capabilities/Settings tab
dragged the Instructions/Skills/MCP rail out of view. Split scrolling on
md+ like settings-page.tsx: the outer container stops scrolling and each
pane scrolls itself. Below md the page still scrolls as one, and the
Overview/Work views keep whole-page scrolling.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(settings): improve API tokens page guidance and token-created dialog
The tokens page never told users what to do with a created token, and
the created dialog was easy to dismiss before the token was saved.
Page:
- Describe how a token is used (multica login --token, Bearer for API)
- Add a security note (full account access, treat like a password)
- Add an empty state instead of rendering nothing
Token-created dialog:
- Clearer title and an info callout emphasizing the token is displayed
only once
- Ready-to-copy CLI sign-in command (multica login --token <token>)
- "I have securely stored this token" checkbox gating the Done button
- Wider dialog (sm:max-w-xl); token and command render on one line and
truncate with an ellipsis (copy buttons still copy the full value)
All four locales (en, zh-Hans, ja, ko) updated.
* fix(settings): show load-failed state instead of empty card when token fetch fails
* fix(settings): add aria-labels to token dialog copy buttons
A machine-level rename stamps the same custom_name on every runtime of a
daemon (MUL-4217), so the settings-page runtime dropdown rendered N
indistinguishable machine-name rows. Split selection into two levels:
pick the machine first, then a runtime on it, labelled by the runtime
itself. Opening lands inside the selected runtime's machine, and the
trigger now reads 'Claude · <machine>' instead of the bare machine name.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The in-page find bar (absolute, z-20) and the resolve collapse bars
pinned at the timeline's top-0 (sticky, z-20) tied on z-index, so the
later-in-DOM collapse bar painted over the find bar, half-hiding it and
orphaning its close button. Raise the find bar to z-30 so the transient
overlay reliably paints above every sticky affordance in the content
column (comment headers z-10, collapse bars z-20).
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>