mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 22:54:38 +02:00
agent/lambda/a18e01ce
2977 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cd93a241aa |
feat(inbox): filter notifications by agent
Adds a single-select agent filter dropdown to the inbox list header. Filters client-side (before dedupe) so agent-sourced notifications aren't dropped when a newer non-agent notification exists for the same issue. Only surfaces agents that have actually sent something into the current inbox. Refs MUL-2156 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cde3867d3b |
feat(sidebar): top/bottom scroll fade mask (MUL-2150) (#2536)
* feat(sidebar): top/bottom scroll fade mask (MUL-2150) Apply useScrollFade to SidebarContent so the menu list softly fades into the header / footer when overflowing, matching the existing pattern used in chat list and onboarding steps. Co-authored-by: multica-agent <github@multica.ai> * fix(ui): useScrollFade re-evaluates on content mutations ResizeObserver only fires on the observed element's own box. When a flex / auto-height container's children grow asynchronously (sidebar pinned items loading from TanStack Query, collapsibles expanding), scrollHeight changes but clientHeight does not — mask stayed 'none' until the user scrolled. Add a MutationObserver on childList to recompute fade when content is inserted or removed. Co-authored-by: multica-agent <github@multica.ai> * test(paths): include squads in workspace route consistency check main added the squads parameterless route to paths.workspace() in #2505 but the C4 consistency assertion wasn't updated, turning frontend CI red on every PR. Add 'squads' to both the parameterless-method set and the segment-mapping table. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8f40a61f8b |
fix(paths): add squads to consistency-test expected set (unblock CI) (#2538)
#2505 (Squad MVP) added paths.workspace(slug).squads() / squadDetail()
to paths.ts but didn't update paths/consistency.test.ts, whose first
test enumerates ALL parameterless workspace route methods and compares
the actual Set to an explicit expected Set. Squads landed on main, the
test started flagging the unexpected extra entry, and the @multica/core
test job has been red since
|
||
|
|
c6ccc49650 |
feat(runtimes): add Tokens metric toggle to Usage Daily chart (MUL-2148) (#2537)
The runtime Usage page's Daily timeline only showed daily $ cost, which
hides the underlying usage shape: cost varies wildly by model price, so
a quiet day on Opus can outspend a busy day on Haiku. Add a Cost/Tokens
toggle next to the Daily/Hourly/Heatmap tabs that swaps the chart over
to a four-segment stack of raw token counts (input / output / cache
read / cache write).
No backend changes needed — the existing /api/runtimes/{id}/usage
response already carries the per-day per-model token breakdown; this
just wires up DailyTokensChart on top of the dailyTokens aggregate that
aggregateByDate was already producing.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
29082f7cfe |
feat: implement Squad feature MVP (#2505)
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
623d29f276 |
feat(agents): one-click create from curated templates (Phase 1) (#2520)
* docs(agents): three-phase agent quick-create plan
Captures the full design for moving agent creation from manual form +
one-by-one skill attachment to a tiered experience:
- Phase 1 (this PR): one-click curated templates, AI-free.
- Phase 2 (next): AI-recommended skills via the existing quick-create
task mechanism — no new server-side LLM dependency.
- Phase 3 (later): AI creates the whole agent end-to-end, composing
Phase 2 with a new `multica agent create` CLI driver.
Documents the architectural decisions that keep all three phases on
existing infrastructure (no SSE, no server-side LLM SDK, no new WS
channels), the two soft blockers Phase 1 unlocks for later phases
(createSkillWithFiles TX composability + skill same-name dedupe), and
the scope decisions we explicitly opted out of (Anthropic plugin
marketplace, ClawHub UI affordances).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skills): harden import against invalid UTF-8 and binary files
PG rejects two byte patterns in a TEXT column. Both crashed real skill
imports we hit while assembling the template catalog:
- Embedded NUL (0x00) -> SQLSTATE 22021. Already stripped by
sanitizeNullBytes, kept as-is.
- Other invalid UTF-8 (e.g. 0x91 — Windows-1252 smart quote in a skill
whose author saved prose from Word). sanitizeNullBytes now also runs
strings.ToValidUTF8 over the content so the second class no longer
takes the whole import down.
For non-text payloads (images, fonts, archives, compiled binaries),
sanitization isn't the right fix — agents never read those as text,
and the bytes can't survive a TEXT column at all. addFile now skips
them by extension before the per-bundle cap counters tick, logging
the skip so an unexpected drop leaves a breadcrumb.
Function name kept for compatibility with the many call sites; both
behaviours are strict supersets of the original.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): split createSkillWithFiles for tx composition + add workspace find-or-create query
Two soft blockers cleared so create-from-template (next commit) can
fold N skill creates and the agent + binding writes into one outer
transaction:
1. createSkillWithFiles used to Begin/Commit its own tx. Caller
composition was impossible — N invocations meant N separate
transactions and no atomicity over the whole materialise step.
Pull the body into createSkillWithFilesInTx(ctx, qtx, input); the
original function becomes a thin wrapper that manages its own tx
for standalone callers. Existing call sites: zero behaviour change.
2. Add GetSkillByWorkspaceAndName sqlc query — workspace skill lookup
by name, anchored to UNIQUE(workspace_id, name) from migration
008. Lets the template materialiser implement find-or-create:
reuse the workspace's existing skill row when a template
references the same name, rather than crashing on the unique
constraint or polluting the workspace with `<name>-2` clones.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): agent template catalog + create-from-template endpoint
Server-side foundation for Phase 1 of the quick-create roadmap (see
docs/agent-quick-create-plan.md). Adds:
- server/internal/agenttmpl/ — embed-loaded catalog of curated agent
templates. Each template ships pre-written instructions plus a list
of skill URLs that get materialised into the workspace at create
time. Validation runs at startup (init() panics on a malformed
template) so a bad JSON ships as a deploy-time defect, not a
runtime 500. Slug must equal the filename basename so the URL
router is mirror-symmetric with the file layout.
- 11 starter templates covering Engineering / Writing / Building /
Testing (code-reviewer, frontend-builder, planner, docs-writer,
one-pager, html-slides, full-stack-engineer, …).
- Three new endpoints, all behind RequireWorkspaceMember:
GET /api/agent-templates — picker list (no instructions)
GET /api/agent-templates/:slug — detail with instructions
POST /api/agents/from-template — materialise + create
Create flow:
1. Auth + runtime authorization happen BEFORE the GitHub fan-out
so a 403 never wastes 20s of upstream fetches.
2. Pre-flight dedupe by cached_name reuses workspace skills
without an HTTP fetch — second create-from-the-same-template
drops from 20s to <100ms.
3. Parallel fetch (30s per-URL timeout) for the remaining skills.
4. Single transaction: every skill insert, the agent insert, and
the agent_skill bindings. On any upstream fetch failure the TX
rolls back and the API returns 422 with `failed_urls` so the
UI can name the bad source(s).
5. extra_skill_ids (user-supplied additions) are verified through
GetSkillInWorkspace per id before attach, so a malicious client
can't graft a skill from another workspace via UUID guessing.
- multica agent create --from-template <slug> CLI flag dispatches to
the new endpoint with a 60s ceiling, matching `multica skill import`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): one-click create-from-template UI
Frontend half of Phase 1. CreateAgentDialog becomes a state machine
spanning four steps:
chooser → Start blank / From template cards
blank-form → existing manual form (post-chooser)
duplicate-form → existing form pre-filled from a duplicated agent
template-picker → grid of templates, click navigates to detail
template-detail → instructions + skill list preview + one-click Use
Picking a template never lands on the form: name auto-deduped against
existingAgentNames, runtime = first usable one, visibility = private.
Refinement happens on the agent detail page if needed. Same rationale
the doc spells out — templates exist precisely to skip configuration.
New components, all collapsible-by-default so quick-create stays fast:
- template-picker.tsx — categorised grid, lucide icons + semantic
accent tokens resolved through static maps so Tailwind's JIT picks
up every variant (dynamic class strings would silently miss).
- template-detail.tsx — instructions preview, skill list with cached
descriptions, Use CTA. Renders the failedURLs banner when a 422
fires — the only step that can trigger that response.
- instructions-editor.tsx — collapsed preview-card / expanded full
ContentEditor.
- skill-multi-select.tsx + skill-picker-list.tsx — shared multi-
select surface, also adopted by the existing skill-add-dialog.
- avatar-picker.tsx — agent avatar upload, mirrors the inspector's
visual language.
Schema-defended client (CLAUDE.md → API Response Compatibility): the
three new endpoints are wired through parseWithFallback with lenient
zod schemas. Desktop builds outlive any given server — a future
field rename / wrapping must not white-screen older installs.
listAgentTemplates accepts both the current bare array and a future
{templates: [...]} envelope. Coverage: 7 new schema-test cases in
schema.test.ts (null body, missing skills/instructions, malformed
create response, envelope migration).
Catalog + detail go through TanStack Query with staleTime: Infinity —
workspace-independent static data, no per-mount refetch.
Other:
- skill-add-dialog becomes a true multi-select (Confirm button +
checkbox list); attached skills are filtered out of the list.
- agents-page hands the freshly-created Agent back to the dialog so a
follow-up setAgentSkills can attach the form-selected skills.
- agent-overview-pane drops the mx-auto/max-w-2xl frame on config-
tab content; the wider dialog visual language reads better with
tabs filling the column.
- Every new UI string lives in both en/agents.json and
zh-Hans/agents.json under create_dialog.* / tab_body.skills.* —
locales/parity.test.ts blocks drift in CI.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): align skill import test + drop next-only lint suppression
- TestFetchFromSkillsSh_ResolvesRootLevelSkillMd now expects assets/logo.png
to be skipped; matches the new addFile binary-extension guard
(
|
||
|
|
19c40c5d68 |
fix(ui): translate hardcoded English strings in shared ui package (#2526)
The four user-visible strings exposed by packages/ui rendered untranslated on every page that used them: - file-upload-button.tsx — "Attach file" aria-label/title - sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title - pagination.tsx — "Go to previous/next page" aria-labels - CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip Root cause: the package had no i18n hookup at all because the package boundary rule forbids importing @multica/core. Replicating the pattern five times would have been the same hack five times. Hooking up react-i18next directly is the structurally clean fix — i18next is a generic library, not business logic, and the upstream I18nextProvider already exposes the instance via context. To let packages/ui typecheck the selector form standalone (i.e. without the views resource-types augmentation in scope), the augmentation is split: views declares everything except the `ui` namespace on a new global `I18nResources` interface, and packages/ui contributes the `ui` slice via declaration merging in packages/ui/types/i18next.ts. Views' resources-types side-effect-imports that file so both packages see the merged shape during downstream typechecks. Scope intentionally excludes: - packages/ui/components/common/error-boundary.tsx — keeping its fallback in English so a render-time crash never depends on i18n being healthy. - apps/desktop/src/renderer/src/components/update-notification.tsx — ships with the next desktop release, not via this PR. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
454c8e3d1a |
feat: in-app preview for non-image attachments (#2528)
* feat(storage): add GetReader to Storage interface Adds a streaming read method to the Storage abstraction so callers can pull object bytes without forcing a full in-memory load. S3Storage wraps GetObject; LocalStorage opens the file with path-traversal and sidecar guards. Tests cover happy path, traversal rejection, sidecar rejection, and missing key. Used in the next commit by the attachment-preview proxy endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add attachment preview proxy endpoint GET /api/attachments/{id}/content streams the raw bytes of a text-previewable attachment back to the client. Exists to (a) bypass CloudFront CORS, which is not configured on the CDN, and (b) bypass Content-Disposition: attachment which Chromium honors for iframe document loads. Media types (image/video/audio/pdf) intentionally do NOT go through this endpoint — clients render them directly from the signed CloudFront download_url, which is already served with Content-Disposition: inline. Hard cap: 2 MB. Larger files return 413. Anything outside the text whitelist returns 415. The whitelist (isTextPreviewable) mirrors the client-side dispatcher; the cross-reference comment in file.go flags the manual sync until a JSON SSOT generator lands. Response always uses Content-Type: text/plain; charset=utf-8 so a hostile HTML payload can't be re-interpreted as a document. The original MIME ships via X-Original-Content-Type for client dispatch. Cache-Control: no-store so revoked attachment access takes effect immediately on the next request. Tests cover happy path (md), extension fallback when content_type is generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and the isTextPreviewable table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core/api): add getAttachmentTextContent + preview error types Adds an ApiClient method that fetches the text body of an attachment via the new /api/attachments/{id}/content proxy. Two typed errors — PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the preview modal render specific fallbacks instead of a generic failure. Refactors the private fetch() into a shared fetchRaw() helper so the new method inherits the standard infra: auth headers, 401 → handleUnauthorized recovery, X-Request-ID, error logging, and the ApiError contract. The previous draft bypassed all of these by calling window.fetch directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(views/editor): add AttachmentPreviewModal + Eye entry points In-app preview for non-image attachments. An Eye icon now sits next to the existing Download button on file cards / readonly file cards / the standalone AttachmentList. Clicking it opens a full-screen modal that dispatches by content_type: pdf: <iframe src={download_url}> — Chromium PDFium video/*: <video controls src={download_url}> — native controls audio/*: <audio controls src={download_url}> — native controls md: <ReadonlyContent> — full markdown pipeline html: <iframe srcdoc sandbox=""> — fully restricted text: <code class="hljs"> — lowlight highlight Media types render directly from the signed CloudFront download_url (server marks them inline-disposition). Text types fetch through the new /api/attachments/{id}/content proxy via TanStack Query, wrapped in useAttachmentPreview() so each entry point owns its own modal state without depending on a global Provider mount. Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger than create-issue's max-w-4xl since PDF / video need room, but capped to viewport on small screens. Sub-renderers use h-full to follow the fixed modal height instead of viewport-relative units. Images are intentionally NOT touched — the existing ImageLightbox (extensions/image-view.tsx) already handles them correctly. The new modal would be churn without user-visible benefit. Adds i18n keys under attachment.* (en + zh-Hans) and registers Preview/Download/Upload in the conventions glossary so future translations stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(desktop): enable Chromium PDF viewer for attachment preview Adds webPreferences.plugins: true to the main BrowserWindow so the bundled Chromium PDFium plugin activates inside iframes — required for the attachment preview modal's PDF dispatch. Default is false in Electron; without it <iframe src=*.pdf> renders blank. Security trade-off, accepted intentionally and documented inline: 1. This window already runs with webSecurity: false + sandbox: false, so plugins: true does NOT meaningfully widen the renderer's attack surface beyond what is already accepted. 2. The only PDFs that reach an iframe here are signed CloudFront URLs we ourselves issued; user-supplied URLs are routed through setWindowOpenHandler → openExternalSafely and cannot land in this renderer. 3. Chromium's PDFium plugin is itself sandboxed and only handles application/pdf — no Flash/Java/other historical plugin surfaces. If we ever tighten webSecurity / sandbox, the follow-up is to host the PDF viewer in a dedicated BrowserView with plugins scoped to that view, keeping the main renderer plugin-free. Old desktop builds ship without the preview modal, so the Eye button never appears and PDF preview is gated by the same release — zero regression risk for users on stale clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
abfe33f350 |
docs: add May 13 changelog (#2529)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.2.32 |
||
|
|
26924dcc98 |
fix(desktop): restore Multica icon + WM_CLASS on Linux (MUL-2145) (#2525)
Closes the regression reported in https://github.com/multica-ai/multica/issues/2515 that PR #2437 only half-fixed in v0.2.31. Two gaps remained on Ubuntu/GNOME: 1. The .deb shipped only the source 1024×1024 PNG under /usr/share/icons/hicolor/, with no usable smaller sizes. GNOME's hicolor lookup walks 16…512 and falls back to the theme default when none match, so the launcher had no icon. The auto-generation pass in electron-builder silently produced only the source size for us. Drop pre-rendered 16/24/32/48/64/128/256/512 PNGs into build/icons/ and point `linux.icon` at the directory so packaging stops depending on the toolchain re-running that generation correctly. 2. WM_CLASS at runtime was `@multica/desktop`, while the .desktop file declared `StartupWMClass=Multica`. PR #2437 assumed Electron derives WM_CLASS from electron-builder.yml's `productName`, but Electron reads `app.getName()`, which reads the *packaged ASAR's* package.json — productName if present, otherwise name. Our source apps/desktop/package.json had no top-level productName, so the ASAR carried only `name: "@multica/desktop"` and Chromium emitted that as WM_CLASS, breaking the .desktop association and the dock icon. Fixed in two anchors for belt-and-braces: add `"productName": "Multica"` to apps/desktop/package.json (so the ASAR carries it and app.getName() resolves correctly by default), and call `app.setName("Multica")` in the production branch alongside the existing dev-only setName so a future regression in package.json or the build pipeline cannot silently re-break WM_CLASS. The `StartupWMClass: Multica` declaration in electron-builder.yml stays pinned and the surrounding comment has been rewritten to record the correct WM_CLASS derivation. Verification on a real Ubuntu install: - `dpkg-deb -c multica-desktop-*-linux-amd64.deb | grep hicolor` lists ≥8 sizes. - `xprop WM_CLASS` on the running window prints `"multica", "Multica"`. - Launcher and dock both show the Multica logo with no manual ~/.local/share/icons workaround. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e2802a5407 |
fix(chat): commit rename only on real outside click, not on hover (#2527)
Base UI's Menu uses focus-follows-cursor — hovering a sibling row drags DOM focus to that row, which made the rename input's onBlur=save fire just from moving the mouse. The result: clicking the pencil and then nudging the cursor would silently commit a half-typed title. Replace the blur handler with a document-level pointerdown listener (capture phase, so it runs before Base UI's outside-click close handler unmounts the input). The listener only commits when the user actually clicks somewhere outside the input. Enter still commits, Escape still cancels, mouse hover is now a no-op. MUL-2110 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5db96b4007 |
fix(daemon): bypass Gemini folder-trust gate in headless mode (#2516) (#2523)
Gemini CLI's folder-trust feature throws FatalUntrustedWorkspaceError (exit code 55) when the current workspace isn't in `~/.gemini/trustedFolders.json` and the process is headless — no interactive trust prompt is available. The daemon spawns gemini with `-p` + `--yolo` in a freshly checked-out worktree that the user has never trusted interactively, so every run with `security.folderTrust` enabled fails after ~10s with exit status 55 and no useful output. Default `GEMINI_CLI_TRUST_WORKSPACE=true` on the child env to short- circuit `checkPathTrust` in gemini-core. This mirrors gemini-cli's documented `--skip-trust` flag; the env var has been gemini's documented headless escape hatch for the entire folder-trust feature lifetime so the fix works on every gemini version that can produce the crash. Callers that explicitly set the same key in cfg.Env win, preserving the ability to opt back into the gate. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
178cfb5008 |
fix(daemon): strip Windows chcp noise from runtime version (#2516) (#2521)
The gemini CLI's Windows shim emits `Active code page: 65001` (from `chcp`) to stdout before the real version reaches `--version` output. The daemon stored the raw concatenation as the runtime version, so the runtime detail page rendered `Active code page: 65001 0.42.0` instead of `0.42.0`. Scan `<cli> --version` line by line and return the first line carrying a semver-shaped token. Full strings like `2.1.5 (Claude Code)` or `codex-cli 0.118.0` survive unchanged; unparseable output falls back to the trimmed raw value. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
51aa924124 |
feat(chat): support renaming chat sessions inline (#2522)
Adds a pencil icon next to the trash icon on each session row in the chat
dropdown. Clicking it turns the title into an inline editable input:
Enter / blur saves, Escape cancels.
Server: new PATCH /api/chat/sessions/{id} handler that updates the title
via the existing `UpdateChatSessionTitle` sqlc query, broadcasts a new
`chat:session_updated` WS event so other tabs / devices stay in sync, and
rejects blank titles. Frontend mutation is optimistic with rollback,
matching the existing delete-session pattern.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
384ddcbe65 |
fix(execenv): seed user-installed Codex skills into per-task CODEX_HOME MUL-1626 (#2519)
* fix(execenv): seed user-installed Codex skills into per-task CODEX_HOME Codex is the only daemon runtime whose HOME is redirected — the daemon sets CODEX_HOME to a per-task isolated directory so each task gets a clean config slate without polluting ~/.codex/. Side effect: the codex CLI never sees the user's `~/.codex/skills/` and tells the user no skill was found. Other runtimes (claude / copilot / opencode / pi / cursor / kimi / kiro) don't have this issue: they leave HOME untouched and discover both user-level skills (from ~/.<runtime>/skills) and workspace-assigned skills (written to a workdir-local dotfile dir) natively. Codex is the outlier. Fix: in execenv.Prepare and execenv.Reuse, copy each subdirectory under `~/.codex/skills/` into the per-task `codex-home/skills/` before writing workspace-assigned skills. Workspace skills still win on sanitized-name conflict; user-level installer symlinks (lark-cli style) are followed so the per-task home gets real content rather than dangling links. Closes #1922 Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): wipe per-task codex skills dir before each hydration Without this, the Reuse path leaves two classes of stale state behind: 1. Round 1 seeded user skill `writing/drafts/stale.md`. Round 2 reuses the same workdir with workspace skill `Writing` assigned: seed stage skips user `writing` (reserved), workspace stage writes `SKILL.md` via MkdirAll + WriteFile but never clears the directory, so the round-1 user support files surface under the workspace skill — violating "workspace fully wins on name conflict" and potentially leaking user-level files into a workspace skill view. 2. User uninstalls a skill from ~/.codex/skills between two runs. The prior copy in codex-home/skills/<name>/ lingers, so the codex CLI keeps seeing the removed skill. Fix: RemoveAll(codex-home/skills) at the start of hydrateCodexSkills, then re-seed user skills and re-write workspace skills. On Prepare this is a no-op (envRoot was already wiped); on Reuse it resets the slate. Added two regression tests covering both scenarios. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6a48022123 | fix(desktop): prevent tab close router sync loop (#2393) | ||
|
|
81b62fc8d3 |
fix(chat): eliminate Skeleton flash on new-chat first message (#2518)
In a new chat (no active session), the first send momentarily rendered ChatMessageSkeleton before the user's message appeared. Root cause: ensureSession called setActiveSession(newId) immediately after creating the session, *before* handleSend wrote the optimistic message to the chatKeys.messages(sessionId) cache. useQuery's first subscription to the new key saw no data → isLoading=true → showSkeleton rendered for one frame. Apply TanStack Query's "seed the cache before subscription" pattern: move setActiveSession out of ensureSession and into the callers, after they've primed the messages cache. handleSend writes the optimistic user message first, then flips activeSessionId; handleUploadFile seeds an empty array first, then flips. useQuery's first read hits cache synchronously and ChatMessageList mounts directly — no Skeleton frame. This is a distinct race from the chat-done flicker fixed in #2509 (unmount/mount on reply completion); both share the same prime-before- subscribe shape. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e8c2855746 |
fix(chat): collapse chat-done flicker via inline cache write (#2509)
* fix(chat): collapse chat-done flicker via inline cache write
The chat panel flickered at end-of-turn: live TimelineView unmounted →
short blank + scroll jump → persistent AssistantMessage finally appeared.
Root cause: chat:done's WS handler called setQueryData(pendingTask, {})
synchronously while invalidateQueries(messages) was an async refetch.
The render guard pendingAlreadyPersisted (chat-message-list.tsx:62-68)
expected the persisted message to already be in the messages cache
before pending cleared, but the sync/async ordering broke that guard.
Fix follows TkDodo's "combine setQueryData (active query) + invalidate
(others)" pattern. ChatDonePayload now carries the freshly-persisted
ChatMessage (id, content, elapsed_ms, created_at); the WS handler
writes it into chatKeys.messages BEFORE clearing pending. Same render
tick → AssistantMessage mounts before TimelineView unmounts → no
flicker. invalidate(messages) stays as a fallback for clients that
took the older code path or for content drift (redaction, etc.).
Also slim task:completed's chat branch — chat:done already wrote the
message and cleared pending; task:completed only refreshes the
cross-session pending aggregate that drives the FAB.
Field additions are all `omitempty` / TS `?:` so older clients ignore
them and older servers (no fields populated) fall back to invalidate-
only, preserving prior behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(chat): cover chat done cache handoff
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>
Co-authored-by: Eve <eve@multica-ai.local>
|
||
|
|
157498e9fa |
fix(editor): preserve pasted mentions in instruction editor (#2514)
`disableMentions` previously skipped registering BaseMentionExtension entirely, which removed the `mention` node type from the editor's schema. Pasting any ProseMirror slice from another Multica editor (clipboard `text/html` carries `data-pm-slice`) caused ProseMirror to silently drop the mention nodes and any surrounding inline text glued to them. Keep the extension registered in all cases. When `disableMentions=true`, attach an inert suggestion (`allow: () => false`) so typing `@` still does not pop the picker — matching the original product intent for agent system prompts — but existing mentions pasted in survive and render as the normal pill. Earlier attempt #2477 patched the paste classifier instead and broke in a different way (`mention://` href tripped the markdown link validator), which led to revert #2510. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7fcc8159ba |
fix(desktop): route attachment downloads through Electron native system on Linux (#2441)
* fix(desktop): route attachment downloads through Electron native system on Linux Replaces shell.openExternal with webContents.downloadURL for attachment downloads in the Electron desktop app. On Linux/Ubuntu, opening a CloudFront URL serving Content-Type: text/html via the system browser causes the browser to render the HTML inline instead of downloading. Electron's native downloadURL shows a save dialog and saves the file directly, fixing HTML downloads regardless of Content-Type. * test(views): update desktop download test to match the new downloadURL bridge The test still referenced the old openExternal bridge. Updated it to assert desktopAPI.downloadURL() instead. * fix(desktop): add URL scheme allowlist to download IPC handler Addresses review feedback on PR #2441. The file:download-url IPC handler called webContents.downloadURL directly, bypassing the http/https allowlist enforced by openExternalSafely. Adds downloadURLSafely() alongside the existing openExternalSafely wrapper, reuses the same isSafeExternalHttpUrl check, and extends the ESLint no-restricted-syntax rule to ban direct webContents.downloadURL calls. Also handles nits: observable warning on null mainWindow, removes dead openExternal field from DesktopBridge, adds desktop-branch failure test. |
||
|
|
b87e54850a |
Revert "fix: preserve mention markdown in instruction paste (#2477)" (#2510)
This reverts commit
|
||
|
|
451c46c43f |
refactor(usage): rename Dashboard → Usage + dynamic per-agent leaderboard (#2511)
The page added in #2462 lived at `/{slug}/dashboard` and was titled "Dashboard", which collides with the conventional meaning ("personal landing surface") and doesn't tell new users what the page is for. Its actual contents — token spend, cost, run time, task counts — map cleanly onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that. Renames (user-visible) - Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop memory router) - Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon LayoutDashboard → BarChart3 (page header icon swapped in sync) - Page title in en/zh-Hans - Reserved-slugs: add `usage` to workspace route segments group; `dashboard` stays reserved in the marketing group (back-compat against workspace slug collisions + keeps the name free for a future Home page) - i18n namespace `dashboard` → `usage` across resources-types.ts, locales/index.ts, and the moved JSON files - WORKSPACE_ROUTE_SEGMENTS in editor link-handler - paths.workspace(slug).dashboard() → .usage(), with matching test expectation updates Per-agent leaderboard polish (`packages/views/dashboard/components/ dashboard-page.tsx`) - Card title "Cost & run time by agent" → "Leaderboard" with a 4-way Segmented control: Tokens / Cost / Time / Tasks - Active metric drives row order, progress-bar width, and the emphasised column header / cell — keeping ranking, visual quantity, and column emphasis in lockstep so users always see what's being measured - Default sort = Tokens (most universally meaningful; Cost still one click away) - Project filter dropdown: - Show ProjectIcon next to the selected project + each list item; FolderKanban as the "All projects" fallback (matches ProjectPicker language) - alignItemWithTrigger={false} so "All projects" doesn't get pushed above the trigger and clipped when the header sits at the top of the viewport (was the root cause of "can't re-select All projects" once a project was selected) - max-h-72 to cap the dropdown when workspaces accrue many projects; matches the runtime-detail Select precedent - Folder name `packages/views/dashboard/*` and `DashboardPage` component name intentionally left in place — user-visible rename only, no broad code refactor. Old `/dashboard` routes are not redirected because the page only landed in #2462 (a few days ago); no real users, external links, or desktop-tab persistence have settled on it yet. |
||
|
|
5a9c15bc12 |
fix: preserve mention markdown in instruction paste (#2477)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
06bcc1fab4 |
feat(feedback): add file upload button so users can attach screenshots (#2501)
The editor underneath the feedback textarea already supports image/file upload via paste and drag-drop, but the modal has no visible affordance — users had no way to discover this. Chat input has the same plumbing and exposes it through a paperclip button; mirror the pattern here. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6e371c2233 |
fix(docs): use dotenv code block lang to unblock Vercel build (#2508)
Shiki's default bundle doesn't include the `env` grammar, so MDX prerendering fails with `Language `env` is not included in this bundle.` The two pages added in #2474 used ```env, which broke both Preview and Production deployments of multica-docs. Swap the language tag to `dotenv` (Shiki ships it by default) — same visual result, no Shiki config change needed. Refs MUL-2122 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ff27142b69 |
fix: treat empty output on successful completion as completed, not blocked (#2507)
When an agent completes successfully (exit 0) but produces no text output, the daemon incorrectly classified it as 'blocked'. This is wrong — agents can legitimately complete work via tool calls (posting comments, pushing code) without emitting text output. Change the empty-output path to return status=completed so the task is correctly reported as successful. Fixes MUL-2104 Co-authored-by: yushen <ldnvnbl@gmail.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
96695a79c5 |
feat(dashboard): workspace/project token + run-time dashboard MUL-1882 (#2462)
* feat(dashboard): workspace/project token + run-time dashboard
Add a `/{slug}/dashboard` page showing per-agent token spend and execution
time across the whole workspace, with an optional project filter.
Backend:
- Three new sqlc queries against task_usage + agent_task_queue: daily
usage, per-agent usage, per-agent total run-time. All optionally
scoped to a project via sqlc.narg('project_id'), reaching project
through the issue join.
- Handlers under /api/dashboard return the same wire shape the runtime
page already consumes (model preserved for client-side cost math).
Frontend: - Shared DashboardPage in packages/views/dashboard reusing KpiCard,
DailyCostChart, ActorAvatar, and estimateCost from the runtime page
so the visual style and pricing math stay in lock-step.
- Period selector (7/30/90d), project dropdown, four KPI tiles
(cost, tokens, run time, tasks), daily cost chart, and a combined
"cost + run time by agent" list.
- Routed in both web (app/[slug]/(dashboard)/dashboard) and desktop
(memory router); sidebar nav entry added under Workspace group.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard): drop stale project filter and stop double-counting tasks
Two issues caught in PR #2462 review:
1. Project filter held the previous selection's UUID across workspace
switches and project deletions: the dropdown gracefully showed
"All projects" (because the title lookup missed) while the three
dashboard queries kept forwarding the dead UUID, leaving the UI
looking like a full-workspace view but populated with empty
project-scoped data. Validate the picked UUID against the current
projects list before passing it to the queries.
2. The "by agent" table read its task count from the token rollup,
which is grouped per (agent, model). A single task that spans two
models lands twice and the agent's row reads e.g. "2 tasks" when
the real count is 1. Prefer `ListDashboardAgentRunTime`'s per-agent
distinct count when available; fall back to the token aggregate
only for agents with no terminal run yet (in-flight tasks).
Extract the merge into `mergeAgentDashboardRows` so the precedence
rules are unit-tested directly.
Co-authored-by: multica-agent <github@multica.ai>
* test(dashboard): allocate per-workspace issue.number explicitly
TestDashboardEndpoints creates two issues in the shared fixture
workspace. issue.number defaults to 0 (migration 020), and the table
carries UNIQUE (workspace_id, number), so the second insert raced the
first on the same default and failed in CI.
Allocate MAX(number) + 1 per insert so each row gets a fresh number
without stepping on rows other tests left behind in the same workspace.
Co-authored-by: multica-agent <github@multica.ai>
* feat(dashboard): rollup table + cron-driven aggregation for dashboard
Mirror the per-runtime rollup in `task_usage_daily` (migrations 073/077/082)
to remove the per-request raw aggregation the dashboard was doing.
Migration 084 adds:
- `task_usage_dashboard_daily` keyed on
(bucket_date, workspace_id, agent_id, project_id, model) — the
dimensions the dashboard actually queries, with project_id nullable
via UNIQUE NULLS NOT DISTINCT (PG15+) so "no-project" buckets
upsert cleanly.
- `task_usage_dashboard_rollup_state` watermark table.
- `task_usage_dashboard_dirty` invalidation queue.
- Triggers on agent_task_queue DELETE, task_usage DELETE, and
issue.project_id UPDATE — the cases the updated_at watermark can't
see. The project_id trigger re-attributes existing rollup rows when
a user moves an issue across projects.
- `rollup_task_usage_dashboard_daily_window(from, to)` —
idempotent recompute primitive (same shape as 077).
- `rollup_task_usage_dashboard_daily()` cron entry — own advisory
lock (4244) so it serialises independently of the runtime rollup.
- `task_usage_dashboard_rollup_lag_seconds()` health helper.
Sqlc queries `ListDashboardUsageDailyRollup` /
`ListDashboardUsageByAgentRollup` read from the new table; the handler
dispatches between rollup and raw on a separate
`UseDailyRollupForDashboard` config flag
(`USAGE_DASHBOARD_ROLLUP_ENABLED` env). Same fail-safe default (false →
raw) so operators can roll out independently of the per-runtime flag.
Bucket date is UTC (the dashboard aggregates across runtimes that may
sit in different tzs; there's no single correct local boundary).
Adds `cmd/backfill_task_usage_dashboard_daily` mirroring the existing
per-runtime backfill — operator runs it once before flipping the flag.
Tests: - TestDashboardEndpoints now also exercises the rollup read path
(raw vs. rollup, same project-scoped totals).
- TestDashboardRollupReattributesOnProjectChange verifies the
issue.project_id trigger enqueues both old + new buckets and the
next rollup tick zeroes the old project + populates the new one.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard-rollup): close two invalidation gaps
Two leak paths missed by migration 084 review:
1. Issue cascade DELETE — the atq BEFORE DELETE trigger runs AFTER the
issue row is gone, so `LEFT JOIN issue` returns NULL project_id and
the original-project bucket never gets cleared (issue 077 calls this
out for the runtime rollup but didn't need to act on it). Adds an
`issue BEFORE DELETE` trigger that enqueues using OLD.project_id
while the issue row is still readable.
2. `LinkTaskToIssue` (quick-create task attaching to a real issue post-
completion) UPDATEs `agent_task_queue.issue_id` from NULL to a real
id. Migration 084 only watched DELETE on atq, so usage already
rolled up under the no-project bucket stayed attributed to NULL
forever. Extends the atq trigger to fire on UPDATE OF issue_id too,
enqueueing both OLD (NULL project) and NEW (linked issue's project).
Tests: - TestDashboardRollupClearsOnIssueDelete asserts rollup row drops to
zero after issue delete + rollup tick.
- TestDashboardRollupReattributesOnLinkTaskToIssue verifies tokens
move from the NULL bucket to the project bucket after the UPDATE.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
24a59098d6 |
fix(projects): make GitHub repo list scrollable in Add Resource popovers (#2490)
* fix(projects): make GitHub repo list scrollable in Add Resource popover When a workspace has many GitHub repos, the list in the Add Resource popover extended beyond the visible area with no way to scroll. Add max-h-48 overflow-y-auto to the repos container to enable scrolling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(projects): make GitHub repo list scrollable in create project modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0ef48797ae |
docs: cloud link is 404 since changed (#2478)
Signed-off-by: yihong0618 <zouzou0208@gmail.com> |
||
|
|
eca36fac84 |
fix(github): plumb GITHUB_APP_SLUG / GITHUB_WEBHOOK_SECRET through self-host (#2482)
The GitHub App integration code reads these two env vars and only enables the Connect flow when both are set. .env.example never listed them, and docker-compose.selfhost.yml did not forward them into the backend container, so self-hosters following the integration docs had no working way to turn the feature on. MUL-2107 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3e61c161c |
fix(inbox): show Multica logo for system-actor notifications (#2479)
Notifications from system actors (e.g. GitHub PR closed) were rendering with an "S" initials fallback. The avatar now shows the Multica icon when actor_type === "system", matching the platform's brand. Co-authored-by: multica-agent <github@multica.ai>v0.2.31 |
||
|
|
a0c64aaf65 |
docs: add 0.2.31 changelog (#2476)
* docs: add 0.2.31 changelog Co-authored-by: multica-agent <github@multica.ai> * docs: refine 0.2.31 changelog copy Co-authored-by: multica-agent <github@multica.ai> * docs: rename github integration changelog title Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2e4d6aa3a9 |
docs(integrations): add GitHub PR ↔ issue integration feature page and self-host setup (MUL-2090) (#2474)
- New /github-integration page (EN + zh) covering identifier matching, merge → Done rule, limitations, and full self-host walkthrough (GitHub App fields, env vars, migration, curl probe) - Adds Integrations nav section in meta.json + meta.zh.json - Adds GITHUB_APP_SLUG / GITHUB_WEBHOOK_SECRET to environment-variables (EN + zh) with cross-link - Cross-links from self-host quickstart Next steps Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a02e58b488 |
fix(github): only auto-close issue after all linked PRs resolve (#2470)
* fix(github): only auto-close issue when all linked PRs have resolved Previously, the webhook handler unconditionally moved an issue to `done` as soon as a single linked PR was merged. If a second PR was also linked to the same issue and still open / draft, the issue would close before the work was actually finished. Add `CountOpenSiblingPullRequestsForIssue` and gate the auto-status transition on it: a merged PR advances its linked issues only when no sibling PR linked to the same issue is still in flight. Issues stay put while siblings are open or draft, and the merge that resolves the last in-flight PR is the one that closes the issue. Adds an integration test that opens two PRs against the same issue, merges the first, asserts the issue stays in_progress, then merges the second and asserts the issue advances to done. Co-authored-by: multica-agent <github@multica.ai> * fix(github): re-evaluate auto-close on closed-without-merge events too GPT-Boy review on #2470: gating only the `state == "merged"` branch left one ordering hole. PR-A merges first → issue stays in_progress because PR-B is open; PR-B later closes WITHOUT merging → no event ever re-runs the auto-close check, so the issue is stuck in_progress. Generalise the trigger to every terminal PR event (`merged` or `closed`) and advance the issue only when: - the issue is not already terminal (done / cancelled); - no sibling PR is still in flight (open / draft); - at least one linked PR — current or sibling — actually merged. Rule (3) preserves "user closed every PR without merging → leave the issue alone": if no work was delivered, the user decides what to do. Replace `CountOpenSiblingPullRequestsForIssue` with `GetSiblingPullRequestStateCountsForIssue`, which returns both the in-flight count and the merged count in a single roundtrip. Adds `TestWebhook_ClosedSiblingAfterMerge` (the regression GPT-Boy flagged) and `TestWebhook_AllClosedWithoutMerge` (the negative case guarding rule 3). Refactors the multi-PR webhook helper out of the existing two-merge test so all three multi-PR scenarios share it. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
61ca43835a |
fix(issue-detail): drop virtualization when deep-linking, restore reliable landing (#2472)
Virtualization and precise deep-link landing have fundamentally opposed
contracts: virtualization uses estimated heights for off-screen items,
deep-link needs real heights for everything above the target. Three
prior fix attempts (initial scrollToIndex race, settle-by-silence
observer, 3-pass cooperative scroll) all tried to satisfy both in one
path and none fully stabilized — code/image/mermaid-heavy comments
kept drifting the target after first landing.
Split by user intent instead:
- highlightCommentId set (user came from inbox to read a specific
comment) -> render flat. Every comment mounts, every height is real,
the target id is in the DOM the instant the effect runs. Native
document.getElementById + el.scrollIntoView({block:'center'}) is
semantically identical to a native <a href="#comment-X"> anchor.
- otherwise -> Virtuoso. Browsing mode keeps the first-paint perf win
from #2413 on long timelines.
Deep-link effect collapses to ~22 lines, matching the pre-virtualization
implementation. A shared renderItem function keeps both render modes
consistent. Removes: bootstrapRef, three-pass scrollToIndex effect,
overflow-anchor:none, scrollPaddingTop on container, scroll-margin-top
on every comment wrapper, virtuosoRef + VirtuosoHandle, initialItemCount
prop, useLayoutEffect.
Mermaid gets a 280px skeleton (web.dev CLS guidance) plus a
sessionStorage layout cache keyed by chart-text hash, so the 0px ->
real-height shift no longer drifts the surrounding layout — useful for
both render modes, deep-link or browsing. Pattern matches ant-design/x
#1497 which fixes the same Mermaid drift in their own stack.
Auto-expand a folded resolved thread when the deep-link target is a
reply inside it; without this the target reply stays collapsed and the
user sees only the resolved-bar.
Net: +131 / -245 in issue-detail.tsx. Tests added for the
resolved-thread-reply auto-expand path.
Known follow-ups:
- <ReadonlyImage> aspect-ratio for image CLS (same class as Mermaid).
- Layout heisenbug (page width "abnormal" without devtools open) is
orthogonal to deep-link and survives this PR; needs separate triage.
- 500+ comment cold mount in deep-link mode pays full markdown+lowlight
cost; GitHub takes the same hit and we accept it.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f17acc21de |
refactor(integrations): drop installation list from Settings tab (#2468)
The card displayed a per-installation row (avatar + account_login + "User|Organization · connected <date>") plus a disconnect button. In practice the title regularly fell back to "unknown" because the server's fetchInstallationAccount call doesn't sign App JWT, and the account-level framing also leaked GitHub's data model into the UX — users care about which repos are wired up, not which GitHub account the App is installed on. Collapse the card to: GitHub mark + description + Connect button (plus the "not configured" hint and role gate). Existing installations stay fully manageable from GitHub's own settings page, reachable via Connect. Removes: - installation list + disconnect button + handleDisconnect - useQueryClient / Trash2 / githubKeys imports - five now-dead i18n keys (loading / empty / connected_at / toast_disconnected / toast_disconnect_failed) in en + zh-Hans |
||
|
|
01bcede2ad |
feat(issues): confirm before terminating a single task (#2466)
The two issue-detail surfaces that stop a single agent task — the sticky AgentLiveCard banner and the active rows inside ExecutionLogSection — cancelled on the first click. Task cancellation is irreversible, and a misclick on a long-running run was costly with no way to recover. Both entry points now route through a shared TerminateTaskConfirmDialog (AlertDialog with destructive confirm), mirroring the pattern the Agents list row actions already use for the "cancel all tasks" flow. The running-state note about a few seconds to fully halt is only shown when the task is actually running or dispatched. Chat window pending-pill Stop is intentionally not affected — it is fire-and-forget with the UI clearing optimistically, and a confirm step there would interrupt chat flow. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0e7fa21832 |
fix(runtimes): correct broken docs link to /docs/daemon-runtimes (#2465)
The 'Learn more' link on the Runtimes page pointed to https://multica.ai/docs/runtimes which returns 404. The docs page is published at /docs/daemon-runtimes. |
||
|
|
caeb146bac |
feat(github): GitHub App integration for PR ↔ issue linking (#1817)
* feat(github): GitHub App backend for PR ↔ issue linking - New tables: github_installation (workspace ↔ App install), github_pull_request (mirrored PR state), issue_pull_request (M:N link). - Webhook handler verifies HMAC-SHA256, upserts PR rows, parses issue identifiers from PR title/body/branch and auto-links them. Merging a linked PR moves the issue to done. - Connect/setup endpoints power the zero-config "Connect GitHub" install flow; state token is HMAC-signed so the setup callback can recover the workspace. - Workspace-scoped admin routes for listing/disconnecting installations, plus a per-issue `pull-requests` list endpoint. Co-authored-by: multica-agent <github@multica.ai> * feat(github): UI for connecting GitHub and viewing linked PRs - Settings → Integrations: new tab with Connect GitHub / installations list / disconnect, gated on the deployment having the App configured. - Issue detail sidebar: Pull requests section showing linked PR title, repo, state (open/draft/merged/closed), and author, with deep link to GitHub. - Real-time refresh: github_installation:* and pull_request:* events invalidate the matching TanStack Query caches. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address review — null actor, role gating, configured guard, scoped uninstall broadcast - listeners: use optionalUUID(e.ActorID) so the system actor on the github-driven issue:updated event no longer panics activity / notification listeners; merged-PR → issue done now produces a status_changed activity and inbox entry. - IntegrationsTab: gate the admin-only installations query on canManage so members no longer hit /github/installations 403; the configured/not-configured copy is also scoped to admins. - backend: introduce isGitHubConfigured() requiring both GITHUB_APP_SLUG and GITHUB_WEBHOOK_SECRET, and surface that single flag from list-installations + connect endpoints so the frontend Connect button stays disabled until both are set. - DeleteGitHubInstallationByInstallationID now RETURNs workspace_id; webhook handler publishes github_installation:deleted scoped to the right workspace so already-open Settings tabs invalidate in real time. ErrNoRows on a re-fired delete short-circuits cleanly. - tests: focused webhook integration coverage (auto-link + merge → done, cancelled preservation, uninstall returns workspace). Co-authored-by: multica-agent <github@multica.ai> * fix(github): i18n the new GitHub UI strings to satisfy lint CI flagged every literal string in the Integrations tab, the Pull requests sidebar section, and the per-PR row label. Move them through useT() and add the matching `integrations.*` block to settings.json (en / zh-Hans) plus `detail.section_pull_requests` / `detail.pull_request_state_*` / loading + empty copy under `issues.json`. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f08b2b4f50 |
fix(attachments): harden local sidecar serving and tighten Upload gate (#2459)
Follow-ups to #2444: - ServeFile refuses keys ending in .meta.json so the sidecar JSON isn't a stable read API. Sits before any disk work so a crafted .meta.json sibling can't trigger an out-of-tree read. - ServeFile rejects paths that resolve outside uploadDir (via filepath.Rel) before readLocalMeta runs. http.ServeFile's own .. guard fires later on r.URL.Path, but readLocalMeta would otherwise do a stray disk read on <some-path>.meta.json before the 400 lands. - Upload only writes a sidecar when filename is non-empty. ServeFile only reads the filename anyway, so a content-type-only sidecar was dead disk weight. - Drop the dead json.Marshal error branch — marshaling two strings cannot fail. Three new tests cover sidecar suffix rejection, the traversal guard, and the no-filename Upload short-circuit. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
91bdec9a54 |
fix(attachments): preserve original filename on /uploads/* downloads (#2444)
LocalStorage.ServeFile delegated straight to http.ServeFile without setting Content-Disposition, so downloads of local-storage attachments landed on disk under the UUID-based storage key instead of the human filename the uploader had chosen. The S3 backend already sets Content-Disposition on PutObject (s3.go:186-187), so the local backend was the only one losing the original filename — a sibling asymmetry that's been there since multi-backend support landed. Upload now writes a sidecar <key>.meta.json beside the data file capturing the original filename and sniffed content type. ServeFile reads the sidecar when present and sets Content-Disposition using the existing sanitizeFilename + isInlineContentType helpers, mirroring the S3 inline/attachment decision exactly. Uploads from before this lands have no sidecar and fall through to the previous behavior. Delete now removes the sidecar alongside the data file so the upload directory doesn't grow orphans. Closes #2442 |
||
|
|
a1c2d53939 |
fix(chat): keep editor mounted across lazy session creation (#2457)
The first file upload in a brand-new chat showed the blob preview for
a moment and then disappeared — the upload looked like it had failed
even though the attachment was actually saved.
Root cause: `<ContentEditor key={draftKey}>`. `draftKey` includes
`activeSessionId`, and `handleUploadFile` (chat-window.tsx) awaits
`ensureSession("")` before forwarding the file to the upload handler.
Lazy-create flips `activeSessionId` from null to a uuid mid-upload,
which changes `draftKey`, which forces React to remount the editor.
The blob image node inserted by `uploadAndInsertFile` was on the old
editor instance; by the time the upload settled, the swap-to-CDN-URL
walk in file-upload.ts couldn't find the blob src in the new editor
and finally `URL.revokeObjectURL` released the blob — broken image.
The create-issue modal has the same draft-store pattern but does not
hit this bug because it never sets a `key` on its ContentEditor; the
editor lives for the lifetime of the modal regardless of draft churn.
Split the two concerns the previous `draftKey` was conflating:
- `draftKey` (zustand storage key) keeps `activeSessionId` so each
session gets its own draft slot — unchanged behaviour.
- `editorKey` (React identity key) drops `activeSessionId` and only
varies on `selectedAgentId`, which is the actual signal Tiptap's
Placeholder needs to refresh on agent switch.
Now the editor stays mounted across the lazy session creation. The
blob preview survives long enough for the swap to find it, and the
user sees the image render normally on the very first upload of a new
chat.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
da03c83251 |
fix(modals): correct text input height in issue creation dialog (#2434)
* fix(modals): correct text input height in issue creation dialog Fixed text input height for both agent and manual create issue dialogs: - Agent dialog: added flex to outer div and flex-1 to inner div - Manual dialog: added flex to description container and flex-1 to editor Fixed: #2433 * fix(editor): make EditorContent a proper flex container - EditorContent: flex flex-1 flex-col - Remove min-height: 100% from .ProseMirror CSS - Let flex-grow handle height consistently across the chain Fixed: #2433 --------- Co-authored-by: ayakabot <ayakabot@seepine.com> |
||
|
|
23c05f13c4 |
refactor(feedback): replace generic description with brand-colored GitHub CTA (#2455)
* refactor(feedback): replace generic description with brand-colored GitHub CTA The Feedback modal previously rendered three lines of grey copy before the editor — title, description, and the GitHub hint from #2451. The hint blended into the description, defeating its purpose of nudging users toward a tracked channel. Drop the generic description (placeholder already explains what to type) and restyle the hint so GitHub itself is the only brand-coloured anchor. The shorter sentence ("Want faster traction? Head to GitHub") puts the link at the natural end-of-line fixation point, where the colour shift actually registers. i18n splits into prefix + link (suffix would be empty), avoiding the sentence-order brittleness that 3-key splits usually introduce. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * copy(feedback): expand GitHub hint to highlight discussion as well Reviewer feedback: "faster traction" only signals speed; users also care about having an open back-and-forth on a tracked thread. Update the hint to surface both benefits without lengthening the line meaningfully. - EN: "Want faster handling and open discussion? Head to GitHub" - ZH: "想被更快处理、参与讨论?请去 GitHub" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> 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> |
||
|
|
b21f69f31a |
fix(views): land deep-link via cooperative scroll passes (#2452)
Replaces #2452's first attempt (placeholder-freeze, 800ms blank window) and the multi-observer settle pipeline from #2449. Both were trying to land the target with a single perfectly-timed scroll, which doesn't compose with how virtualization actually works. The non-virtualized version of this code, pre-#2413, was 12 lines: one el.scrollIntoView once timeline.length > 0 && !loading. That worked because every comment was in the DOM, so the target's absolute position was real, not estimated. Virtualization breaks that invariant — Virtuoso renders a window, fills the rest with spacer heights derived from estimates, and the target's offset is spacer-sum until each above-target item is mounted and measured for the first time. Those measurements arrive in waves: viewport mount, ResizeObserver pass, markdown render, lowlight code highlight, image load. Each wave updates spacers and shifts the target's offset by tens to hundreds of pixels. The previous two attempts both tried to detect "settle" and land once. ResizeObserver on the target watches the symptom, not the cause (#2449). Rendering placeholders to freeze the cause shows 800ms of blank where comments should be (#2452 v1). This rewrite cooperates with Virtuoso's own measure→correct loop instead of trying to outrun it. Three scrollToIndex calls — t=0, t=120 (after the first measurement wave), t=500 (after markdown / lowlight settle) — let the convergence narrow on each pass. Each call uses whatever spacer heights are current; differences across passes are typically a few pixels (cold viewport) to a few dozen (big code blocks), not the full-spacer drift that motivated placeholders. Visually it reads as a single instant scroll with at most a couple of subtle re-centerings, not a re-jump. initialTopMostItemIndex stays — it's the only API that anchors position *before* first paint, and it's the reason cold-start deep-links from inbox land at the target without a visible "scroll from top". Captured exactly once via a useRef one-shot following React's documented "avoid recreating ref contents" idiom, so #458's persistent-anchor reset behavior can't trip. Crucially we now spread-on-defined rather than passing `={undefined}` — react-virtuoso crashes with "Cannot read properties of undefined (reading 'index')" on the latter because the library accesses .index on the prop without a null guard. Net delta vs main: −86 lines. Deletes ~150 lines of the #2449 MutationObserver/ResizeObserver settle pipeline plus this PR's prior placeholder/deepLinking/flushSync machinery, replaces with ~30 lines of straightforward effect + bootstrap ref. The whole deep-link path is now smaller than the original pre-virtualization version was, because the convergence loop is explicit and the correctness story doesn't require auxiliary state. Refs: react-virtuoso #458 (initialTopMostItemIndex anchor reset), #883 (initial scroll race), #1083 (scrollTop model divergence vs native scrollIntoView). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
723489d2a9 |
feat(feedback): nudge users toward GitHub for discussion and faster traction (#2451)
Add a small CTA below the Feedback modal description that links to github.com/multica-ai/multica/issues for users who want a tracked, public channel. The in-app feedback form still serves vague impressions and weekly-aggregated input; GitHub is for concrete bugs, feature requests, and discussion that benefits from community visibility. i18n covers en + zh-Hans following the conventions.zh.mdx voice guide (full-width punctuation, ASCII ellipsis, spaces around Latin terms). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
86aa5199fc |
feat(chat): support attachments & images in chat input (#2445)
* docs(plans): chat attachment & image support implementation plan Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(db): add chat_session_id/chat_message_id to attachment Co-authored-by: multica-agent <github@multica.ai> * feat(db): sqlc — chat_session_id on CreateAttachment + LinkAttachmentsToChatMessage Co-authored-by: multica-agent <github@multica.ai> * feat(file): upload-file accepts chat_session_id form field Co-authored-by: multica-agent <github@multica.ai> * feat(chat): SendChatMessage links uploaded attachments to the new message Co-authored-by: multica-agent <github@multica.ai> * feat(api): uploadFile accepts chatSessionId; sendChatMessage accepts attachmentIds Co-authored-by: multica-agent <github@multica.ai> * feat(core): useFileUpload supports chatSessionId context Co-authored-by: multica-agent <github@multica.ai> * feat(chat): support paste/drag/upload attachments in chat input Co-authored-by: multica-agent <github@multica.ai> * test(e2e): chat input attachment upload + send round-trip Co-authored-by: multica-agent <github@multica.ai> * chore(chat): keep lazy-created session title empty so untitled fallback localizes Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review — dedupe ensureSession + parse upload response - chat-window: cache in-flight createSession promise in a ref so a file drop followed by a quick send no longer spawns two sessions (and orphans the attachment on the losing one). - Attachment type + EMPTY_ATTACHMENT + AttachmentResponseSchema: include the new chat_session_id / chat_message_id fields the server now returns. - uploadFile: route the response through parseWithFallback so a malformed body returns EMPTY_ATTACHMENT instead of an undefined-keyed Attachment, matching the API boundary rule. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address PR #2445 review — test ctx, send gating, attachment surface 1. Backend test was 400ing because the handler reads workspace from middleware-injected ctx, and `newRequest` only sets the header. Helper `withChatTestWorkspaceCtx` mirrors the agent-access-test pattern and loads the member row + SetMemberContext before invoking the handler. 2. Attachment metadata now flows end-to-end: - new sqlc `ListAttachmentsByChatMessageIDs` (batch lookup, mirrors the comment-side query) - `chatMessageToResponse` takes `attachments` and `ChatMessageResponse` surfaces them — same shape as CommentResponse - `ListChatMessages` loads them via a new `groupChatMessageAttachments` helper so the chat bubble can render file cards - daemon claim path pulls `ListAttachmentsByChatMessage` for the latest user message and ships `ChatMessageAttachments` to the daemon - `buildChatPrompt` lists id+filename+content_type and instructs the agent to `multica attachment download <id>` — fixes the private-CDN expiring-URL problem where the markdown URL would have expired by the time the agent acts - TS `ChatMessage` gains an optional `attachments` field 3. Chat composer now blocks send while uploads are in flight: - `pendingUploads` counter increments in handleUpload, SubmitButton uses it to disable - handleSend also gates on `editorRef.current.hasActiveUploads()` to catch the Mod+Enter path that bypasses the button - new vitest covers the "drop large file → immediate send" scenario where attachment id would otherwise be silently dropped Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore: drop implementation plan doc Process artefact, not something the repo needs to keep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> 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> |
||
|
|
208f1ddb29 |
fix(views): land virtualized deep-link via settle-by-silence (#2449)
The earlier deep-link fix (
|
||
|
|
b58567ed6c |
fix(desktop): restore Multica app icon on Linux (#2437)
Fixes three gaps in the Linux desktop build that combined to render the
Multica window with the system Settings (gear) icon on Ubuntu:
1. Force `linux.executableName: multica` so the scoped npm name
`@multica/desktop` stops leaking into `executableName`, the `.desktop`
filename, the `Icon=` field, and `/usr/share/icons/hicolor/*/apps/*.png`.
The leading `@` in the previously-generated `@multicadesktop` violates
freedesktop desktop-entry naming, breaking GNOME's window↔.desktop
association and forcing the theme-default icon. (The artifact-filename
side of the same scoped-name leak was already patched in 10618b1f;
this commit closes the desktop/icon-identity side.)
2. Always set `BrowserWindow({ icon })` on Linux — previously gated on
`is.dev`. AppImage direct-launches never install the `.desktop` entry,
so without an explicit window icon the WM has no other path to the
bundled image. The resolved path now points into `app.asar.unpacked/`
(matching the existing `bundledCliPath()` convention in
`daemon-manager.ts`) since the Linux native icon code path requires a
real filesystem path, not an asar-internal one.
3. Pin `linux.desktop.entry.StartupWMClass: Multica` explicitly. The
value already matches the productName-derived default, so this is a
build-time no-op today, but it makes the WM_CLASS↔StartupWMClass
matching contract auditable in config — future changes to
`productName` or `app.setName()` now show up as a diff against this
file instead of silently re-breaking the icon association.
Fixes https://github.com/multica-ai/multica/issues/2424.
|
||
|
|
bb312002d1 |
docs(self-hosting): document Caddy WebSocket essentials (#2436)
* docs(self-hosting): document Caddy WebSocket essentials Add a single-domain Caddy example and harden the separate-domain one with the WebSocket route a self-hoster actually needs: - handle /ws* (prefix match, not exact `/ws`) so future path variants don't fall through to the frontend block - flush_interval -1 inside the WS reverse_proxy, otherwise frames sit behind Caddy's default flush window and surface as "comments only appear after a page refresh" Both gaps were hit by a self-hosted user on a single-domain Caddy deployment, and neither was documented. Co-authored-by: multica-agent <github@multica.ai> * docs(self-hosting): tighten Caddy /ws matcher to avoid catching `/ws-*` slugs Use a named matcher `path /ws /ws/*` instead of the over-broad `handle /ws*`. Caddy's `*` is a path-glob without segment boundary, so `/ws*` would also match unrelated paths like `/ws-foo` — which is a legitimate workspace URL under the current reserved-slug rules (only the exact `ws` slug is reserved). Per GPT-Boy review on PR #2436. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |