Drop the hand-crafted aurora gradient approximation and inline the
canonical "Google Gemini" path from Simple Icons (CC0 1.0), rendered
in the Simple Icons brand color (#8E75B2). This matches the pattern
used by the other provider marks in this file (Claude/Codex from
Bootstrap Icons, etc.) instead of trying to manually approximate the
official multicolor wash from gemini.google.com (which paints via a
clipPath over an embedded raster).
Co-authored-by: multica-agent <github@multica.ai>
Swap yellow/green radial gradient anchors so colors land at the official
positions: top red / right blue / left yellow / bottom green, matching
gemini.google.com's current aurora spark. Replace the arc-based 4-point
spark outline with a cubic-bezier version normalized to the 24-viewBox
so the inset between tips is smoother and closer to the gstatic source.
Co-authored-by: multica-agent <github@multica.ai>
Per review on PR #2904: the previous 3-stop blue/purple/pink gradient
was the legacy Bard-era Gemini spark. Update to the 5-stop cyan → blue
→ purple → pink → orange gradient used by the current Gemini app/web
multicolor mark.
Co-authored-by: multica-agent <github@multica.ai>
Gemini provider was falling through to the default Monitor icon in the
runtime list. Add the official 4-point spark mark with Google's
blue → purple → pink gradient, matching the SVG style/sizing of the
other provider icons.
Co-authored-by: multica-agent <github@multica.ai>
* feat(autopilot): support assigning autopilot to a squad (MUL-2429)
Path A (Squad-as-Leader) from the RFC: when an autopilot's assignee is a
squad, dispatch resolves to squad.leader_id and executes against the
leader's runtime — semantics match a human manually assigning the issue
to that squad, no fan-out.
Backend scope only; frontend picker change is a follow-up PR.
Changes:
- 096_autopilot_squad_assignee migration: drop agent FK on
autopilot.assignee_id, add assignee_type column (default 'agent'),
add autopilot_run.squad_id attribution column.
- service.AgentReadiness: single source of truth for archived /
runtime-bound / runtime-online checks. Shared by autopilot
admission gate, run_only dispatch, and isSquadLeaderReady.
- service.resolveAutopilotLeader: translates assignee_type/id to the
agent that actually runs the work.
- dispatchCreateIssue: stamps issue with assignee_type='squad' for
squad autopilots and enqueues via EnqueueTaskForSquadLeader.
- dispatchRunOnly: belt-and-braces readiness re-check after resolving
squad → leader so a leader that went offline between admission and
dispatch produces a clean failure instead of a doomed task.
- handler.CreateAutopilot / UpdateAutopilot: accept assignee_type with
squad/agent existence + leader-archived validation. Backward-compatible
default of "agent" preserves the contract for older clients.
- Analytics: AutopilotRunStarted/Completed/Failed events carry
assignee_type and squad_id; PostHog can now group autopilot runs by
squad without joining back to the autopilot row.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): reject archived squads, route post-admission skips, cleanup dangling-agent autopilots (MUL-2429)
Addresses three review findings on PR #2888:
1. Archived squad handling: validateAutopilotAssignee now rejects squads
with archived_at set; resolveAutopilotLeader returns errSquadArchived
so the admission gate fails closed; DeleteSquad now mirrors the issue
transfer for autopilot rows (TransferSquadAutopilotsToLeader) so
surviving autopilots flip to assignee_type='agent' (leader) instead
of dangling at the archived squad.
2. dispatchRunOnly post-admission readiness: introduces errDispatchSkipped
sentinel, recognised by DispatchAutopilot via handleDispatchSkip so
the run is recorded as `skipped` (not `failed`). Manual triggers no
longer 500 when the leader's runtime goes offline between admission
and task creation. New TestManualTriggerDoesNotErrorOnPostAdmissionSkip
locks the behaviour in.
3. Dangling agent assignee after migration 096 dropped the FK:
shouldSkipDispatch now distinguishes pgx.ErrNoRows / errSquadArchived
(hard skip — retrying won't help) from transient DB errors
(fail-open). DeleteAgentRuntime pauses autopilots that target agents
about to be hard-deleted (ListArchivedAgentIDsByRuntime +
PauseAutopilotsByAgentAssignees) so the breakage surfaces as a paused
row in the UI instead of a quiet skip-burning loop.
Unit tests cover the sentinel unwrap contract and errSquadArchived
errors.Is behaviour. Integration test
TestAutopilotDispatchSkipsWhenRuntimeOffline re-verified against a fresh
DB with migration 096 applied.
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilot): bump last_run_at on post-admission skip (MUL-2429)
Match recordSkippedRun (pre-flight skip) and the success path so the
scheduler / "last seen" UI both reflect that this tick evaluated the
trigger, even when the post-admission readiness gate caught a late
regression.
Addresses Emacs review caveat #1 on PR #2888.
Co-authored-by: multica-agent <github@multica.ai>
* feat(autopilot): mixed agent/squad assignee picker in dialog (MUL-2429)
End-to-end UI for assigning an autopilot to a squad. Closes the PR #2888
backend gap: the squad-as-assignee feature was already wired in Go (Path A,
RFC §4) but the desktop dialog never offered the choice.
- core/types/autopilot: add `AutopilotAssigneeType`, surface
`assignee_type` on `Autopilot` + Create/Update request payloads.
- views/autopilots/pickers/agent-picker: switch to a polymorphic
AssigneeSelection (`{type, id}`); render agents and squads as two
grouped sections with shared pinyin search.
- views/autopilots/autopilot-dialog: maintain `assigneeType` state, send
it on create/update, render the trigger avatar / hover dot with
`assignee.type`.
- views/autopilots/autopilots-page + autopilot-detail-page: render the
assignee row using `autopilot.assignee_type` so squad-typed autopilots
show the squad avatar + name, not a broken agent lookup.
- locales: add `agents_group` / `squads_group` / `select_assignee` keys
(en + zh-Hans), keep legacy `select_agent` for callers that still
reference it.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Wires the frontend half of the read-only RFC. The Settings → GitHub tab
now always issues the installation list query for any workspace member
(the backend gates it via `RequireWorkspaceMember` after PR #2886) and
gets `can_manage` straight from the API response. The render matrix
covers the six cases the RFC calls out:
- configured + connected + admin → Disconnect + (optional) Connected by
- configured + connected + member → read-only "Connected to" + read_only_hint
- configured + not connected + admin → Connect button + dev description
- configured + not connected + member → contact_admin_to_connect hint
- not configured + admin → operator banner + disabled Connect
- not configured + member → contact_admin_to_connect hint
New i18n keys (en + zh-Hans): read_only_hint, connected_by, contact_admin_to_connect.
The unused github.manage_hint string is removed (its non-admin branch
now resolves to one of the two new hints depending on connection state).
GitHubInstallation gains an optional `connected_by` display name so the
UI can render the "Connected by {name}" line without further changes
once the backend exposes the field.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(github): expose read-only installation list to workspace members (MUL-2413)
Relax `GET /api/workspaces/{id}/github/installations` from owner/admin-only
to any workspace member so the Settings → Integrations tab no longer renders
blank for non-admins (the original symptom of MUL-2413).
The handler now reads the caller's role from the workspace middleware:
- owner / admin keep the full row including the numeric `installation_id`
(the connect / disconnect handle) and receive `can_manage: true`.
- every other role (member / guest) receives rows with `installation_id`
omitted and `can_manage: false`, giving them visibility into "is GitHub
wired up?" without the management handle.
`GET /github/connect` and `DELETE /github/installations/{id}` stay under
the admin/owner middleware group — this PR only relaxes the read path.
Tests: `TestListGitHubInstallations_RoleGating` exercises admin, owner,
member, and guest paths against the real DB-backed handler fixture and
asserts the field stripping + `can_manage` contract.
Refs: MUL-2413
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): redact installation_id from realtime broadcasts (MUL-2413)
GET /github/installations strips the numeric installation_id for non-admin
members, but the github_installation:created / uninstall / suspend WS
events were still publishing it, so the same handle was reachable from
any workspace client subscribed to the workspace scope. Broadcast both
payload variants without it — the frontend uses these events only to
invalidate the installations query, so admins re-query the list endpoint
to recover the management handle.
Also adds a router-level test that mounts the production middleware split
(member-visible list vs. owner/admin connect+delete) so a future routing
change can't silently widen the write surface.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
In the attachment preview modal, image and video previews used
`max-h-full max-w-full`, which let small assets render at their
natural size and leave the modal mostly empty. Switch to
`h-full w-full` so the preview always occupies the modal viewport,
relying on `object-contain` to preserve aspect ratio without
upscaling beyond the intrinsic bounds.
Only touches `packages/views/editor/attachment-preview-modal.tsx`
for the image (line 355) and video (line 373) branches; pdf, audio,
markdown, html, and text branches keep their existing layout.
Co-authored-by: multica-agent <github@multica.ai>
Tiptap's stock ListItem keymap binds Enter only to splitListItem. When the
cursor sits in an empty top-level list item, splitListItem returns false
(without dispatching) with a code comment saying "let next command handle
lifting" — but no next command is chained. Enter then falls through to
ProseMirror's baseKeymap which inserts another empty paragraph inside the
list item, trapping the user.
Replace StarterKit's ListItem with PatchedListItem whose Enter binding
chains splitListItem → liftListItem via commands.first. The lift fallback
only runs when splitListItem returns false (top-level empty case),
restoring the standard "double-Enter exits the list" behaviour seen in
every other rich-text editor. Non-empty and nested-empty items are
unaffected because splitListItem already handles them correctly.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
## Summary
Adds per-user `profile_description` so coding agents have cheap, durable context about who is asking. v1 per the brief Xeon locked in on [MUL-2406](mention://issue/63a7247c-4f6a-42cf-90d1-7c746e77158a):
- **DB** — `user.profile_description TEXT NOT NULL DEFAULT ''` (migration 096). 2000-rune cap enforced server-side. No nullable / privacy state to manage.
- **API** — `PATCH /api/me` accepts the field; `UserResponse` always emits it. Client wraps `updateMe` in a lenient `UserSchema` + `EMPTY_USER` fallback per CLAUDE.md API Response Compatibility.
- **UI** — Settings → Account gains an "About you" textarea with live `n/2000` counter, `maxLength` guard, and a localized too-long error (EN + zh-Hans).
- **CLI** — `multica user profile get` / `multica user profile update` with `--description / --description-stdin / --description-file / --clear`, mirroring the existing `issue comment add` input-mode menu.
- **Daemon injection** — claim handler resolves the runtime owner and stamps `requesting_user_name` + `requesting_user_profile_description` on the task. `buildMetaSkillContent` emits `## Requesting User` between `## Agent Identity` and `## Available Commands`, blockquoted and framed as background context. The block is omitted entirely when the description is empty (no token cost when unused).
Brief is written **once per task** via `CLAUDE.md` / `AGENTS.md`, not the per-turn prompt — same path the agent already reads for identity, so no extra per-turn cost.
## Test plan
- [x] `go build ./...`, `go vet ./...`, `go test ./internal/cli/ ./internal/daemon/ ./internal/daemon/execenv/ ./cmd/multica/`
- [x] New brief tests: `TestBuildMetaSkillContentEmitsRequestingUser`, `TestBuildMetaSkillContentOmitsRequestingUserWhenEmpty`
- [x] `pnpm typecheck`, `pnpm lint`, `pnpm test` (74 files, 644 tests pass)
- [ ] Handler DB tests (`TestUpdateMe*`) require a migrated test DB — not runnable in this sandbox
- [ ] Manual: open Settings → Account, set a description, confirm the next daemon-run agent's `CLAUDE.md` shows `## Requesting User`
* feat(issues): show project segment in issue breadcrumb (MUL-2422)
Render the issue's project (when present) between the workspace and any
parent-issue segment. Segment reflects the issue's own `project_id` so
the same URL produces the same breadcrumb from every entry point.
Failed/missing project queries fall back to an "Unknown project"
placeholder; loading shows a skeleton to avoid layout shift.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): cap project breadcrumb width to preserve title precedence
Constrain Project crumb to max-w-72 (matching ProjectChip) and add
min-w-0 to the title span so the flex compression order matches RFC
§5/§9: Project/Parent shrink before the current Issue title.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(runtimes): declutter the runtimes page (MUL-2407)
Cuts visual noise on the Runtimes detail view without removing real
information:
- MachineDetail: drop the 4-card metric grid (RUNTIMES / HEALTH /
WORKLOAD / CLI) and replace it with a single inline meta strip. The
cards repeated what the title chip and runtime rows already show.
- PageHeaderBar: remove the inline tagline + "Learn more" link. The
header is now icon + title + count + connect button.
- VisibilityBadge: only render the Public chip. Private is the default,
so a row of `🔒 Private` badges was pure noise.
- CliCell: drop the per-row "Desktop" managed badge — the same string on
every desktop row carried near-zero information.
- MachineSidebar row: hide the truncated daemon-id subtitle. The id is
still available on hover via `title` and remains visible in the
detail header.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): address review feedback on inline meta and hover title
- Inline meta now reads "6 runtimes · 5 online" instead of "6 6 online"
by using runtime_count for the total label.
- Sidebar machine title hover now shows full daemon id (with subtitle
fallback) so the daemon id is recoverable after the sub-row was hidden.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
320px was too cramped for typical rendered HTML (charts, dashboards,
formatted documents). Matches the existing HTML attachment preview
height for visual consistency across both iframe surfaces.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438)
Drops the post-onboarding ImportStarterContent / DismissStarterContent
flow (handler + routes + StarterContentPrompt + templates + locale
strings + analytics event). The bug — web onboarding seeding 6+ starter
issues without a runtime — only existed through that path; with it gone
the source disappears.
The "install a runtime" issue from BootstrapOnboardingNoRuntime is now
the canonical no-runtime onboarding seed. The title/description and a
LockAndFindActiveDuplicate-deduped seeder move to
handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace /
AcceptInvitation seed it whenever the workspace has no runtime yet, so
every mark-onboarded entry point lands the user on a concrete next
step.
starter_content_state column is kept and continues to be claimed as
'imported' in all five entry points so older desktop builds (which
still render the legacy dialog on NULL) don't surface it to accounts
created after this change.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438)
054 only covered pre-feature users. Anyone onboarded between then and the
starter-content kit removal could still sit at NULL, and old desktop
clients gate the legacy StarterContentPrompt on `starter_content_state
IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL
would surface a dialog whose buttons 404. Mark them 'imported' to match
the new helper's claim semantics.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Add optional `opts.activate` to NavigationAdapter.openInNewTab. Default
stays `false` so cmd/ctrl+click on links/mentions keeps browser-style
background semantics. The two explicit toolbar entry points
(attachment-preview-modal, html-attachment-preview) opt in with
`{ activate: true }` so the new tab gains focus after the modal closes.
Both desktop providers (root + per-tab) now use the tab id returned by
`store.openTab` to call `setActiveTab` only when `activate` is true.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881)
Project Gantt now fetches its own scheduled-only data instead of riding the
Board/List pagination cache. The Unscheduled drawer and pagination warning
banner are gone, and any WS-driven issue change (create / update / delete)
invalidates the new cache so the timeline stays live.
- Backend: `GET /api/issues?scheduled=true` adds an
`(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both
ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler.
- Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single
fetch and lives under its own cache key. WS handlers and mutations
invalidate the prefix on create/update/delete so the bar reacts to
start_date / due_date changes from other tabs and from this tab without
waiting on the WS round-trip.
- GanttView: drops the Unscheduled section, the pagination warning banner,
and the load-all button; renders only scheduled rows.
- Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`,
`summarizeIssueListPagination`, and the gantt locale strings that
supported the old plumbing.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): page through Gantt fetch and isolate per-view data sources
- Walk paginated `scheduled=true` issues until total is reached so projects
with more than 500 scheduled bars no longer silently truncate.
- Gantt mode disables the bucketed Board/List query and reads its own
scheduled cache for the project empty-state check, so the page never
short-circuits Gantt with a Board-derived "no issues" CTA.
- `onIssueLabelsChanged` patches matching rows in the Project Gantt cache
in-place, keeping label filters consistent after attach/detach from
other tabs or agents.
MUL-1881
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Earlier the unification commit dragged in a Tailwind override stack
(ring, rounded-md, transition-shadow, bg-background/95, button hover
classes) "to make standalone surfaces work without .rich-text-editor
scope". Because the legacy CSS rules were not removed, both layers
applied in the editor, producing a visible double-stroke selection
ring and a light-theme hover on top of the dark-glass toolbar.
This commit reverts the styling churn:
- ImageAttachmentView now emits the same span-only DOM as the original
ReadonlyImage: <span.image-node> > <span.image-figure> > <img.image-content>
+ <span.image-toolbar> with naked <button> children. No Tailwind tax.
- The `.image-*` rules in content-editor.css are de-scoped from
`.rich-text-editor` so the single set of styles also drives chat /
AttachmentList renders. Editor-only behavior (640px cap, NodeView
centering) stays under the `.rich-text-editor` scope.
- A `data-clickable` attribute carries the "this image is clickable
to preview" hint that the readonly cursor rule used to key off the
`.rich-text-editor.readonly` scope.
- ImageView NodeViewWrapper no longer adds its own `image-node` class
because `<Attachment>` already emits one; the duplicate was harmless
but redundant.
Visual: editor + readonly comments render identical to before. Chat /
AttachmentList previously rendered a gray file card for images (the
P0 fix in the parent commit) and now match the editor visual without
the heavy-handed Tailwind detour.
Tests: 98 attachment-related tests pass; full `pnpm typecheck` + `pnpm
test` (652 tests) green.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent root causes made "Open in new tab" on a desktop
attachment-preview modal feel like "the popup is still there and the
current tab got replaced":
1. `AttachmentPreviewModal.handleOpenInNewTab` never called `onClose()`,
so the modal stayed mounted over the new tab.
2. Both `DesktopNavigationProvider.openInNewTab` and
`TabNavigationProvider.openInNewTab` called
`store.setActiveTab(tabId)` after `store.openTab(...)`, which stole
focus to the new tab — violating the type contract
("Desktop only: open a path in a new background tab") and matching
neither Chrome's cmd+click default nor the user's expectation.
Fixes:
- Modal: always call `onClose()` after dispatching the navigation
(desktop adapter path and web `window.open` fallback path).
- Desktop navigation: drop the post-`openTab` `setActiveTab` call in both
providers. `openTab` already preserves `activeTabId` for new paths and
switches to the existing tab when the path is already open, which is
exactly the background-tab semantics the type contract advertises.
Tests:
- `attachment-preview-modal.test.tsx`: assert `onClose` is invoked on
both the desktop and web fallback branches.
- `pageview-tracker.test.tsx`: rename the "openInNewTab / addTab" case
so the comment no longer claims `openInNewTab` activates the new tab.
- New `apps/desktop/.../platform/navigation.test.tsx`: assert that
`openInNewTab` on both providers calls `openTab` and never
`setActiveTab` for same-workspace paths, and routes cross-workspace
paths through `switchWorkspace`.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
HTML attachment previews mount the document inside a sandboxed
`<iframe srcdoc>` deliberately WITHOUT `allow-same-origin` — uploads are
untrusted user content. Chromium treats fragment-link clicks inside such an
opaque-origin srcdoc iframe as cross-origin frame navigation and silently
rejects them, so clicking a TOC entry never scrolls.
Append a tiny shim script to the srcdoc that intercepts `<a href="#...">`
clicks inside the iframe and calls `scrollIntoView` directly. The shim runs
in the iframe's own opaque origin under `allow-scripts` — no new
capabilities, no sandbox token changes; it cannot reach parent / cookies /
localStorage.
All three HTML attachment surfaces share the same helper:
- inline 480px card (html-attachment-preview.tsx)
- full-screen modal (attachment-preview-modal.tsx)
- full-page route (attachment-preview-page.tsx)
References: whatwg/html#3537, crbug 40191760.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Collapse the five separate attachment render paths (file-card NodeView,
image NodeView, readonly markdown img/fileCard renderers, AttachmentList
standalone fallback, and the parallel packages/ui/markdown renderer) into
one <Attachment attachment={a} /> dispatcher.
Fixes a P0 visual regression: a PNG attached to a message but not inlined
in the markdown body used to render as a gray "file card" because
getPreviewKind() lacked an "image" branch and image rendering bypassed
the dispatcher entirely. Now every surface routes through <Attachment>,
so the same PNG renders as a real <img> with hover toolbar and
preview-modal everywhere.
Key changes:
- PreviewKind gains "image"; getPreviewKind() detects image/* + common
extensions before the html/text branches (so svg stays image, not text).
- AttachmentPreviewModal gains case "image" (replaces the standalone
ImageLightbox, which is deleted).
- New packages/views/editor/attachment.tsx owns all kind-aware routing
(image | html | file) and dispatches preview modal + download via the
existing useAttachmentPreview / useDownloadAttachment hooks. Subsumes
the deleted AttachmentBlock.
- AttachmentInput.url accepts a forceKind hint so callers that *know*
the structural kind (markdown , Tiptap image node) skip the
filename-based autodetect — fixes a regression where empty or
descriptive alt text would route an image to the file-card chrome.
- Tiptap NodeViews (file-card.tsx, image-view.tsx) shrink to thin
wrappers that forward editor hints (selected, deleteNode, uploading)
to <Attachment>.
- ReadonlyContent and AttachmentList each mount their own
AttachmentDownloadProvider so url → record resolution works outside
ContentEditor's provider.
- packages/ui/markdown gains optional renderImage / renderFileCard slot
props; packages/views/common/markdown.tsx injects <Attachment> into
those slots and threads message attachments through to chat /
skill-file viewers.
- chat-message-list passes message.attachments to every <Markdown> call
site and renders a standalone AttachmentList under each bubble for
attachments not referenced in the body.
Tests: attachment.test.tsx covers 9 scenarios (record image / pdf / html;
url-only image with resolver hit and miss; uploading state; editable
delete; forceKind regression). attachment-preview-modal.test.tsx gains
image-dispatch cases. 652/652 unit tests pass.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a header toggle that lets users flip the agent transcript between
chronological (oldest first, current behavior) and newest-first. The
preference is persisted via a small Zustand store. Default stays
chronological so existing readers see no behavior change.
Sort is a pure presentation concern — the underlying timeline (seq
numbers, filter keys, segment navigation) is untouched. Toggling resets
the scroll container to the top so the user lands on the newest end of
the chosen direction. Copy-all respects the displayed order so the
exported text matches what's on screen.
Scope is limited to the task transcript dialog per the MVP plan; the
issue execution log and agent activity tab are out of scope and may be
revisited once this interaction validates.
Closes GH #2736.
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): add Project Gantt view (MUL-1881)
Adds Gantt as a third option in the Project page's view toggle (Board /
List / Gantt). Bars span start_date → due_date; issues with only one
date render as markers, issues with neither are collapsed into an
Unscheduled section. Toolbar exposes day/week/month zoom and a
show-completed toggle. The Gantt view shares the existing IssuesHeader
filters/sort.
Implementation is self-rendered SVG/HTML — no new dependencies. UTC
day-aligned date math keeps bars on the right columns regardless of
viewer timezone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): scope Gantt to project surface + warn on hidden pages
- IssuesHeader / IssueDisplayControls now take `allowGantt` (default false);
only Project Detail opts in. /issues, /my-issues and the actor panel no
longer expose a Gantt option that silently fell through to List, and the
toggle icon falls back to List when a stored `viewMode === "gantt"` lands
on a surface that doesn't render it.
- Project Gantt now surfaces a banner with hidden-issue count plus a
Load-all action that drains every remaining paginated page into the
cache via the new `useLoadAllRemaining` helper. Pagination summary comes
from `myIssueListPaginationOptions`, which shares the existing cache key
with `myIssueListOptions` so totals stay in sync with Board/List.
- ScheduledRow normalizes a `start_date > due_date` anomaly to min/max and
outlines the bar with a destructive ring + tooltip note, instead of
silently dropping the row.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): simplify runtime bootstrap
* fix(onboarding): close private-helper reuse hole and guide-issue nav race
- server: when bootstrap looks for an existing Multica Helper, require
Visibility="workspace" so a private helper owned by another member
can't be auto-assigned to the onboarding issue (and trigger a task as
that private agent), which would have bypassed canAccessPrivateAgent.
- web onboarding page: refreshMe() inside bootstrap flips hasOnboarded
before onComplete fires, letting the guard's router.replace overtake
onComplete's router.push to the new guide issue. Mark the page as
"completing" right before navigating so the guard stays silent during
the in-flight transition.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): escape daemon command literals to satisfy i18next/no-literal-string
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Lambda <lambda@multica.ai>
The two `<code>` blocks in the "having trouble?" disclosure of the
Connect Remote dialog render literal shell commands ("multica daemon
status" and "multica daemon logs -f"). The `i18next/no-literal-string`
rule (enforced as error across packages/views) flagged them, turning
@multica/views#lint red on main since the dialog landed.
These strings are inherently locale-agnostic — they are the actual
commands users type into a shell, identical in every language. Wrapping
them in t() would be wrong (translators would have no source-of-truth
about whether the binary name `multica` or the subcommand `daemon` could
be translated; the answer is "never").
Mark them as exempt with `eslint-disable-next-line i18next/no-literal-string`
+ a one-line comment explaining why. Mirrors how shell-command snippets
are treated elsewhere in the repo.
Verification:
- `pnpm --filter @multica/views lint` → 0 errors (was 2). 13 remaining
warnings are pre-existing in other files and don't fail CI.
- Cascaded failures (@multica/views#typecheck, web/desktop builds) on CI
were strictly downstream of the lint failure; they'll go green once
lint passes.
* feat(settings): allow editing workspace issue prefix (MUL-2369)
Workspace admins can now change the issue prefix from Settings → General.
The change is gated by a confirmation dialog that warns about external
references (PR titles, branch names, links) breaking, because issue
identifiers are rendered as `prefix-N` on the fly — changing the prefix
effectively renames every existing issue.
Refs https://github.com/multica-ai/multica/issues/2797
Co-authored-by: multica-agent <github@multica.ai>
* fix(settings): invalidate issue cache when workspace prefix changes (MUL-2369)
Issue identifiers (`MUL-123`) are recomputed from `workspace.issue_prefix`
at read time, so cached issues kept showing the old `OLD-N` keys after a
prefix change. Without invalidation the confirm dialog's "all issues will
be renumbered" promise was broken until a hard refresh — and other tabs
receiving the `workspace:updated` WS event saw the same drift.
- WorkspaceTab: after a prefix-changing save, invalidate `issueKeys.all`
in addition to the workspace list. Non-prefix saves stay cheap.
- Realtime: split `workspace:updated` out of the generic `workspace`
refresh into a specific handler that compares cached vs incoming
`issue_prefix` and invalidates issues only when it actually changed.
- Docs: align the "uppercase" language with the actual UI/backend rule
(uppercase letters and digits, up to 10 chars).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(dashboard): add 1d time range to workspace Usage tab
1d means "today" — the natural calendar day from 00:00 UTC, matching the
rollup's bucket_date axis — not the trailing 24 hours. The client-side
dailyCutoffIso filter is now applied in daily dim too so 1d collapses
strictly to today even at the midnight UTC edge where the server's
wall-clock since cutoff would otherwise include yesterday.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard): scope `1d` to today only on aggregate endpoints
The pre-aggregated `byAgent` / `runTime` dashboard endpoints leaked
yesterday into the agent leaderboard and KPI cards for the `1d` time
range because `parseSinceParam(days=1)` returned `now-24h` (wall clock)
and the downstream SQL then applied `DATE_TRUNC('day', @since)`, which
landed on yesterday 00:00 UTC. The PR's client-side `dailyCutoffIso`
filter could only fix the date-bearing daily endpoints; aggregate
responses are already collapsed across dates.
Anchor `parseSinceParam` at UTC start-of-today instead, so `days=N`
covers N natural calendar days (today + N-1 prior). This matches the
frontend `dailyCutoffIso = today - (days-1)` semantic that the
workspace dashboard already assumes, and removes the off-by-one that
previously made `30d` return 31 buckets.
The runtime-detail page uses `parseSinceParamInTZ` (timezone-aware),
which is unchanged — it has no `1d` option.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): per-question v2 questionnaire (source/role/use_case)
Replaces the 3-questions-on-one-screen gate with three lightweight,
individually-skippable steps. New step order:
welcome → source → role → use_case → workspace → runtime → agent → first_issue
- New v2 questionnaire schema: source/role/use_case + per-slot
`*_skipped` markers. `team_size` removed.
- Click-to-advance card grid with lucide + emoji icons (RFC Option B).
- Skip is a footer text button; Other expands a free-text input.
- Recommendation table updated for new role × use_case vocabulary,
with use_case-only fallback when role is skipped.
- DB migration v1 → v2 maps existing role/use_case answers and drops
team_size; historical nulls stay null (not retroactively skipped).
- Re-entry treats skipped slots as fresh; analytics record kept in DB.
- onboarding_questionnaire_submitted event payload updated:
source replaces team_size, per-slot skip booleans added.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): tighten question UX (Continue, layout, brand icons)
Address review feedback on Source/Role/Use-case:
- Replace auto-advance with an explicit Continue button so selections
are reviewable. Continue is disabled until something is picked (and,
for Other, until the free-text input is non-empty).
- Move Back/Skip/Continue inline under the option grid; drop the
duplicate Back from the top header — the page now has a single,
anchored action row.
- Swap the placeholder lucide marks for real brand SVGs on Source:
Google, X, LinkedIn, YouTube, and an OpenAI mark for the AI-assistant
option. Generic options stay on lucide.
- Replace the awkward expanded underline input on the Other card with
an inline borderless input that swaps in for the label slot, so the
Other state has the same height and weight as the other cards.
E2E smoke test updated to click Continue between question steps.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): unify step nav, rename Runtime step around "where agents run"
- Refactor the Source/Role/Use case questionnaire steps to use the same
3-region chrome (header with Back + step indicator, scrolling main,
sticky footer with Skip + Continue) that Workspace/Runtime/Agent
already use, so the Back/Skip/Continue affordances stay in the same
on-screen position across the whole flow.
- Reframe the Runtime step around the user-visible question — "Where
will your agents run?" — instead of the internal "runtime" concept.
The aside panel keeps the educational "What's a runtime?" copy for
users who want to learn.
- Drop the hard-coded "Step 3 · Runtime" eyebrow on the web fork step:
Runtime is now step 5 of 7 after the per-question split, and the
step indicator already shows the correct count.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): tighten Skip/Continue spacing in step footer
Group Skip and Continue inside a sub-flex with gap-2 so they read as a
single action cluster on the right, while the status hint still anchors
left via mr-auto. Applied to both the questionnaire steps and the
runtime step so the footer layout stays consistent across onboarding.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): move Skip/Continue inline below form, drop sticky footer
The sticky bottom footer left a large dead zone between the form
content and the action buttons — most onboarding steps only fill the
top third of the viewport. Move the hint + Skip + Continue inline,
directly below the form/options grid, so the buttons sit where the eye
already is after picking an option.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): match Skip button size to Continue (size="lg")
Skip used the default button size (h-8) while Continue used size="lg"
(h-9), so the two adjacent action buttons rendered visibly different
heights. Promote Skip to size="lg" in step-question and
step-runtime-connect so they line up.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): reframe step 3 as 'connect a computer' / 'pick an agent runtime'
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): replace cloud waitlist with "Coming soon", reword CLI intro
- Web Step 3 cloud card: remove "Join waitlist" CTA + dialog and render a
static "Coming soon" badge instead. Drops CloudWaitlistDialog, the
cloud DialogState, waitlistSubmitted local state, and the
onWaitlistSubmitted prop on StepPlatformFork (desktop's
StepRuntimeConnect still owns its own waitlist path).
- Tighten cloud_subtitle to drop the "join the waitlist" half now that
the action is gone.
- cli_install.intro: "AI coding tool" → "agent runtime", EN + zh-Hans.
Tests updated to match: asserts the Coming soon badge is non-actionable
and drops the four cloud-dialog scenarios (now unreachable).
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): refresh button, "agent runtime" wording, coming-soon card
Three fixes on the desktop Step 3 empty state per review:
1. Empty headline + hints now say "agent runtime", matching the
picker-context terminology established earlier in this PR.
2. Add a Refresh button (header pill in Found, inline with the
headline in Empty). Desktop wires it to restart the bundled
daemon so a freshly-installed Claude/Codex/Cursor CLI is picked
up — the daemon's PATH probe runs once at boot, so without a
restart the install would only take effect on next launch.
3. "Use a cloud computer" loses the waitlist dialog and renders as
a disabled "Coming soon" badge, aligning with the web fork.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): address review follow-ups (i18n, step-order, version, tests)
- runtime-aside-panel: point "Learn more" to /docs/install-agent-runtime,
branching by language so zh users land on /docs/zh/...
- zh-Hans: unify Cloud "Coming soon" wording to "即将推出"; translate
step_workspace.preview.more_meta ("and more" -> "等等")
- onboarding-flow: derive forward navigation from ONBOARDING_STEP_ORDER
via advanceFrom(curr) so inserting/reordering a step only requires
editing the canonical array; runtime → agent/first_issue branch keeps
its bespoke routing with a comment explaining why
- onboarding handler: gate questionnaireAnswers.complete() on
Version == 2 so a future schema bump can't be silently mis-counted
against v2 funnel semantics
- add unit tests for step-source / step-role / step-use-case (option
click, Skip patch, Other free-text) and step-question shell
(canContinue + pendingOther state machine)
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): rename useCaseFallback to fallbackFromUseCase
ESLint's react-hooks/rules-of-hooks treats any function starting with
"use" as a React hook. The helper is a pure switch — give it a name
that doesn't trip the rule.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Tab 3's semantics were widened in #2829 to surface issues assigned to
either an owned agent OR a squad the user belongs to / leads. The label
still said "我的智能体" / "My Agents", which under-described the new
scope. Rename to "我的智能体和小队" / "My Agents and Squads" so the tab
title matches what it filters.
Locale-only change. Filter logic, SQL, and other tabs untouched.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Re-introduces the `involves_user_id` filter on the issues list / open-list /
count / grouped paths, but with the semantics nailed down for the second time
around: tab 3 surfaces issues whose assignee is an *indirect* extension of the
user (owned agent, or a squad they're a human member of / lead via owned agent
/ have an owned agent inside) — and explicitly NOT direct member assignment,
which is tab 1's meaning.
- server/pkg/db/queries/issue.sql: 4-branch filter on ListIssues /
ListOpenIssues / CountIssues. Each subquery clamps workspace_id because
issue.assignee_id is polymorphic with no FK. Leader resolution reads
squad.leader_id directly, not the squad_member copy row (squad.go ignores
errors when seeding that copy, so it can be missing). FindActiveDuplicateIssue
switched from positional $2/$3/$4 to named sqlc.arg() — pure hygiene so the
generated struct field names don't drift when new nargs are added.
- server/internal/handler/issue.go: parse involves_user_id and plumb it into
the three sqlc params; ListGroupedIssues (hand-written dynamic SQL) gets a
mirrored 4-branch fragment, no shortcut.
- packages/core: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter /
api.listIssues / api.listGroupedIssues all carry the new param through.
- packages/views/my-issues: tab 3 switches from client-side agent-fanout to
involves_user_id=user.id. agentListOptions import and the myAgentIds memo
go away.
- server/internal/handler/issue_involves_test.go: 13 integration tests cover
every branch (positive + cross-workspace negatives) plus the critical
ExcludesDirectMemberAssignee negative on BOTH the sqlc and the grouped paths,
locking tab 3 ∩ tab 1 = ∅.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtimes): weekly usage dimension + tz-aware aggregation (MUL-2382)
Adds a Weekly view to the runtime Usage chart alongside Daily and Hourly,
backed by `aggregateByWeek` on the existing 180-day daily cache (no new
endpoint). Weeks are ISO 8601 Mon–Sun; the in-progress week is rendered at
half opacity and tooltip-labelled "partial · N / 7 days".
Side effects called out in the RFC:
- `sliceWindow` now reads "today" in the runtime's IANA timezone, fixing a
one-day drift at the window edge when the browser and runtime sit in
different time zones.
- ActivityHeatmap rows are reordered Mon → Sun to match the rest of the
Weekly aggregation; "today" is computed in runtime tz so the grid's
trailing column lines up with the daily rows the backend buckets.
Dimension / period coupling: switching dimension resets the period to that
dimension's default when the active value isn't in its allowed set
(Hourly 7/30, Daily 7/30/90, Weekly 30/90/180).
Unit tests cover weekStart / addDays / tz-aware today, the sliceWindow
boundary, and aggregateByWeek's partial-week math.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): weekly chart shows trailing calendar weeks (MUL-2382)
aggregateByWeek built one bucket per week-with-data, and the caller
took the last N buckets. With sparse data — old populated weeks plus
empty stretches near today — the slice surfaced the old weeks instead
of the trailing in-window calendar weeks the user selected.
Now aggregateByWeek takes weekCount and emits exactly that many
trailing calendar weeks anchored at today's week in the runtime tz.
Buckets are pre-zeroed so empty in-range weeks render as empty bars;
rows outside the window are dropped.
Co-authored-by: multica-agent <github@multica.ai>
* feat(usage): drop Hourly dim + add Daily/Weekly to workspace dashboard (MUL-2382)
- Remove Hourly from the runtime usage WHEN-chart: segmented control is
now Daily / Weekly. Drop the HourlyActivityChart component,
aggregateCostByHour helper, byHour query subscription, and the
when_tab_hourly i18n key.
- Add the same Daily / Weekly dimension toggle to the workspace-level
Usage page (dashboard-page.tsx). Time-range linkage matches the runtime
page: Daily allows 7/30/90 (default 30), Weekly allows 30/90/180
(default 90); switching dimensions resets `days` when the current value
isn't in the new dimension's set.
- Reuse `aggregateByWeek` from runtimes/utils for cost / tokens
(signature relaxed to accept the wider DashboardUsageDaily shape).
Add `aggregateWeeklyTime` / `aggregateWeeklyTasks` in dashboard/utils
with identical pre-zeroed trailing-week semantics. Workspace dashboard
uses the user-chosen timezone (existing TimezoneSelect) as the
week-boundary tz; runtime page continues to use the runtime's IANA tz.
- New `WeeklyTimeChart` / `WeeklyTasksChart` mirror their daily
counterparts plus partial-week half-opacity bars and rangeLabel
tooltips, matching the existing Weekly cost / tokens charts.
- Tests: drop hourly-related setup; add weekly run-time / tasks coverage
asserting pre-zeroed trailing buckets and the same MUL-2382 sparse
window-scoping regression we caught on the runtime side.
Co-authored-by: multica-agent <github@multica.ai>
* fix(usage): correct workspace Weekly window + lock tz to UTC (MUL-2382)
Two blocking correctness bugs from Emacs's PR #2822 review:
1. The Weekly chart paints `ceil(days/7)` trailing calendar weeks but the
API was still asked for exactly `days`. Worst case (today = Sunday on a
30D request) the leftmost Monday sits 34 days back, so the first week's
bucket was silently truncated. Over-fetch the per-date queries to
`weekCount * 7` days when Weekly is active; per-agent rollups stay at
`days` so the KPI / leaderboard labels keep their advertised window.
Daily-aggregation surfaces (cost/tokens/time/tasks KPIs and the Daily
chart) re-scope the over-fetched rows back to `days` so the labels
stay consistent.
2. The backend dashboard rollup buckets data by UTC `bucket_date` (and the
raw fallback queries by `DATE(tu.created_at)`, also UTC), but the
frontend was driving Weekly boundaries from the user-chosen
`TimezoneSelect`. Near midnight UTC that put cross-boundary rows into
the wrong calendar week. Lock workspace Weekly to UTC and remove the
timezone picker from this page; the runtime detail page keeps its own
`runtime.timezone`-anchored aggregation, which is consistent because
its rollup is materialized in that runtime's tz.
Verification: pnpm --filter @multica/views test (636 passed),
typecheck clean, lint 0 errors / 13 pre-existing warnings.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The inline HtmlAttachmentPreview toolbar carries an "Open in new tab"
button that routes to /{slug}/attachments/{id}/preview. The full-screen
AttachmentPreviewModal was missing the same affordance, so users who
maximized an HTML preview lost the ability to pop it into its own tab.
Mirror the gating exactly: show when kind === 'html' && slug &&
attachmentId. Other PreviewKinds keep the existing header (Download +
Close) — they don't have a corresponding full-page route.
Co-authored-by: multica-agent <github@multica.ai>
* feat(my-issues): cover squad assignees via involves_user_id (MUL-2364)
The "My Agents" tab on /my-issues only resolved agents owned by the
caller, so issues assigned to squads (member, leader, or agent-member of
mine) never surfaced. This added a UNION-based involves_user_id filter
that the backend expands to "me + agents I own + squads I relate to" in
a single query.
- SQL: ListIssues / ListOpenIssues / CountIssues accept narg
involves_user_id and OR a workspace-scoped 3-branch UNION on the
squad assignee subquery. Leader is sourced from canonical
squad.leader_id (not the best-effort squad_member copy row whose
AddSquadMember error is dropped in squad.go:177-188 and :259-263).
- Handler: parses involves_user_id via parseUUIDOrBadRequest, plumbs
into all three list params, and mirrors the same UNION fragment into
the grouped dynamic SQL path.
- Frontend: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter
gain involves_user_id; api client forwards it to the querystring.
- My Issues page: "agents" scope now passes involves_user_id instead of
fanning out owned-agent IDs client-side. Tab label widens to
"我的智能体 / 小队" / "My Agents / Squads".
- Tests: Go suite covers all three squad relations including the
canonical-leader-without-squad_member-copy variant, cross-workspace
isolation for agent / leader / squad_member branches, combination
with creator_id, and the malformed-UUID 400 path. Client test pins
the involves_user_id querystring wiring for both list endpoints.
The FindActiveDuplicateIssue query gets explicit sqlc.arg() names so
sqlc regeneration keeps the existing struct field names regardless of
the local sqlc version (no behavior change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(my-issues): tighten cross-workspace negatives for involves_user_id UNION
Cross-workspace negative tests previously put both the foreign actor and the
foreign issue in the foreign workspace, so the outer i.workspace_id = $1
already excluded the row before the UNION branches were exercised. Stripping
a.workspace_id = $1 / s.workspace_id = $1 from any of the UNION subqueries
would not have failed the tests.
Rewrite the three existing negative cases to seed the issue in
testWorkspaceID with a polymorphic assignee_id pointing at a foreign-workspace
agent or squad (issue.assignee_id has no FK per migrations/001_init.up.sql:61).
Now each UNION branch must enforce its own workspace scoping for the issue to
stay out of the result.
Also add ExcludesOtherWorkspaceSquadAgentMember: the squad_member.agent UNION
branch had only positive coverage; this test pins that s.workspace_id = $1
and a.workspace_id = $1 must both hold there too.
Verified by mutation: stripping the workspace clause from each branch makes
the corresponding test fail.
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>
New docs page covering install pointers, binary names the daemon scans
for, and basic auth notes for all 11 supported AI coding tools. EN +
zh-Hans, registered under "How agents run" in the docs sidebar.
The onboarding "no agent runtime found" empty state now shows an
"Install an agent runtime →" link that opens the new doc, so users have
a discoverable path beyond "skip" and "join waitlist".
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): list-only tasks panel with issue search (MUL-2391)
Replace the agent detail tasks view-mode toggle with a fixed list view and
add a search bar that filters by issue title, identifier, or pinyin.
Co-authored-by: multica-agent <github@multica.ai>
* fix(actor-issues): only show search empty state when searching
Previously the panel rendered the search empty state whenever the
filtered issue list was empty, which masked ListView's own status-based
empty states when status/priority/assignee/project/label filters
narrowed the list to 0. Now search_empty only renders when
`search.trim()` is non-empty and results are 0; otherwise ListView
takes over and shows its native empty states.
Refs MUL-2391
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): bump hast-util-to-html to v9 so lowlight output actually serializes
Source view of fenced ```html (and any other code block falling through to
the lowlight branch in ReadonlyContent) silently rendered as un-highlighted
escaped text. Root cause was a stale dep pin: `hast-util-to-html: ^4.0.1`
predates the package's ESM/named-export rewrite — v4 only exports a CJS
default function, so the `import { toHtml } from "hast-util-to-html"` in
code-block-static.tsx:19 and readonly-content.tsx:32 resolved to
`undefined` at runtime. The try/catch in both call sites caught the
"toHtml is not a function" throw and fell through to escapeHtml plain
text, so no `.hljs-*` spans ever made it to the DOM and the syntax-color
CSS added in #2808 had nothing to attach to.
Bumping to ^9.0.5 (matches the v9 line that lowlight@3 / remark / rehype
ship in the rest of the tree) makes the named `toHtml` export available
and source-view highlighting works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): open HTML attachment in new tab + full-page preview route
Adds a third toolbar button to HtmlAttachmentPreview between Maximize and
Download: open the attachment in a new app tab (desktop) or browser tab
(web). The full-screen modal stays — they serve different scenarios:
modal for a quick "see it bigger" without leaving the issue context,
new-tab when the user wants to keep the rendered HTML around while
working on something else.
Components:
- New workspace path: `/{slug}/attachments/{id}/preview?name={filename}`.
Lives outside the (dashboard) group on web so the iframe gets the full
viewport — sidebar would defeat the point. Desktop registers the route
inside `WorkspaceRouteLayout` so workspace context resolution still
runs (no slug → no path is built).
- `packages/views/attachments/attachment-preview-page.tsx`: shared full-
page view that reuses `useAttachmentHtmlText` for the iframe srcDoc.
Sandbox stays `allow-scripts` (no allow-same-origin) — same security
posture as the inline preview.
- `HtmlAttachmentPreview`: adds Open-in-new-tab button. Routes through
`useNavigation().openInNewTab` when available (desktop), falls back to
`window.open(getShareableUrl(path))` on web. Button is hidden when no
workspace slug is in scope (shouldn't happen in practice, but the
shared component must not throw outside a workspace route).
Tests cover: desktop openInNewTab call args, web window.open fallback,
and that the failure-mode toolbar still surfaces all three actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): drop now-stale @ts-expect-error on hast-util-to-html imports
v9 ships bundled type declarations, so the directives added for v4 trigger
TS2578 ("Unused '@ts-expect-error' directive") on CI typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When alternately switching between manual and agent modes in the create-issue
dialog, the title and description were being duplicated and accumulated on
every round-trip. Root cause: manual→agent packed title+description into the
agent prompt but left them in the shared useIssueDraftStore; the subsequent
agent→manual wrote the agent markdown into draft.description while the stale
draft.title persisted, so the remounted manual panel surfaced both.
Clear title/description from the shared draft at the moment they move into
the agent representation, so round-trips can't layer stale manual state on
top of prompt-as-description.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Two issues from #2790's HTML inline preview work:
1. HTML source view rendered as default-colored text. lowlight emits
`.hljs-tag` / `.hljs-name` for `<...>` brackets and element names, but
content-editor.css only styled the keyword / string / attr / etc.
classes — so toggling an inline ```html``` block to "source" showed
attributes colored and everything else plain. Adds the two missing
classes in light + dark.
2. HtmlAttachmentPreview carried a "Copy code" button. An HTML attachment
is a file (view + download), not an inline source snippet. The inline
```html``` fenced block (HtmlBlockPreview) is where reading / copying
source belongs. Drops the button, its state, and the useAttachmentHtmlText
`canCopy` branch — the hook is still needed for the iframe srcDoc.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ActorAvatar applies bg-muted on its container regardless of whether
an image is loaded, so transparent regions of PNG/SVG avatars reveal
the grey placeholder. agent-detail-inspector also wraps ActorAvatar
in an outer bg-muted div, layering a second grey square.
Make bg-muted conditional on the fallback state in ActorAvatar, and
drop the redundant bg-muted from avatar-picker's image-loaded branch
and the two inspector wrappers. Empty-state placeholders unchanged.
The TriggerRow's outer flex uses `items-start`, which made sense back
when every trigger only had one row of content (label + maybe a cron
expression). Once #2774 added the URL action row to webhook triggers
(Copy + Rotate buttons sitting on a second line inside the inner column),
the trash button stayed pinned to the top-right of the outer flex — it
visibly floats above the URL action buttons instead of lining up with
them, which reads as a layout glitch.
Move the trash button into the URL action row for webhook triggers so
all three action buttons (Copy, Rotate, Delete) share one flex container
and align by construction. Schedule and API triggers — which have no
URL row — keep the trash button pinned top-right (their bodies are
short enough that the top corner reads as "the row's right end").
Extract a `deleteButton` const so the JSX isn't duplicated, and add the
existing `delete_dialog.confirm` i18n string as the title attribute for
consistency with the other action buttons (Copy / Rotate already have
hover titles).
No behavioural change — same click handler, same confirm dialog.
* feat(editor): HTML attachments render like images (MUL-2345 v4)
HTML attachments no longer wear the file-card chrome (icon + filename
row). They now render as a sandboxed iframe with a hover-revealed
right-top toolbar (Open / Download / Copy code), mirroring the image
attachment visual model.
- New HtmlAttachmentPreview owns the iframe + hover toolbar plus three
states (loading / success / error). Failure mode keeps the toolbar
pinned open and Open/Download enabled so the user is never stranded
without an escape hatch — Copy code disables when the text body is
unavailable.
- New AttachmentBlock thin dispatcher picks the renderer per kind:
html + attachmentId + !uploading -> HtmlAttachmentPreview, else
AttachmentCard. All three entry points (file-card NodeView, readonly
file-card, standalone AttachmentList) call AttachmentBlock, so feature
work on a new kind only touches one place.
- AttachmentCard collapses back to a pure file-card row UI: the inline
HTML iframe branch (InlineHtmlIframe + inlineHtmlEnabled +
showInlineHtml) is removed.
- AttachmentBlock added to the editor barrel export.
Sandbox/server-side defenses unchanged: sandbox="allow-scripts" (no
allow-same-origin), srcDoc, server still returns text/plain + nosniff
on the /content proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(editor): pin three entry points to AttachmentBlock HTML route (MUL-2345)
Reviewer flagged that the v4 dispatcher refactor only had tests on the
shared AttachmentBlock + HtmlAttachmentPreview; the three real call
sites at file-card.tsx:59, readonly-content.tsx:279, and
comment-card.tsx:152 had no regression coverage. Reverting any one
would silently lose the inline HTML iframe path — the exact MUL-2330
regression we're meant to be locking down.
Each new test renders the real entry point with an HTML+attachmentId
fixture and asserts the dispatched iframe (sandbox=allow-scripts,
srcdoc) shows up while the AttachmentCard chrome (filename row) does
not. FileCardView and AttachmentList are exported from their files for
direct rendering, mirroring the existing CodeBlockView test pattern.
Mutation-tested locally: temporarily flipping each site back to
<AttachmentCard> turns its corresponding test red.
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>
Two related overflow bugs in the Delivery detail dialog (the popover you
open from a webhook deliveries row, shipped in #2784) became obvious as
soon as a real webhook payload was exercised:
1. **Horizontal overflow: minified JSON pushed dialog off-screen.**
`CodeBlock`'s `<pre>` uses `white-space: pre` (default for the tag),
which means a single-line minified JSON body had intrinsic
min-content equal to the whole line's width. The parent grid cell
inherits the default `min-width: auto` (= min-content), so a long
body propagated all the way up and blew DialogContent past its
`max-w-2xl` cap. Headers rendered fine because they're
pretty-printed JSON with real newlines.
Fix: `min-w-0` on the CodeBlock wrapper so it can shrink below
min-content, plus `whitespace-pre-wrap break-all` on the `<pre>` so
long lines wrap (`break-all` is the only modifier that breaks
mid-token, which a minified JSON body needs because it has no
whitespace to break at).
2. **Vertical overflow: dialog grew past viewport.**
`DialogContent` had no height cap. With Raw body + Headers +
Response body + Replay button stacked vertically, anything beyond
the screen edge (notably the Replay button) became unreachable.
Fix: `max-h-[85vh] overflow-y-auto` on `DialogContent`.
Both fixes are CSS-only in one file; HMR verified.
- Multi-select UI for batch importing skills from a local runtime
- Server batch-dispatches up to 10 import requests per heartbeat cycle
- WS heartbeat now reads supports_batch_import from daemon payload
instead of hardcoding true, so old daemons correctly fall back to
one-at-a-time dispatch
- Raised server pending timeout to 3min and client poll timeout to 4min
to accommodate daemons that pop only one import per 15s heartbeat
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(squads): show member working status on squad detail page
Add a new GET /api/squads/{id}/members/status endpoint that returns each
member's derived working/idle/offline/unstable status, the issues each
agent is currently running, and the last observed activity timestamp.
The Squad detail page's Members tab consumes this snapshot to render a
status pill and an active-issue link next to each agent, with live
refresh wired through the existing task/agent/daemon WS events.
Human members are returned with status=null so the UI can keep them in
the same list without implying a presence signal. Archived agents stay
in the response and surface as offline rather than being filtered out.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): address review feedback on member status endpoint
- i18n the "blocked" issue-status pill in squad members tab (was a
bare literal that failed `i18next/no-literal-string` lint).
- Treat any dispatched/running task as working, even when its
`agent_task_queue.issue_id` is NULL (chat / quick-create tasks).
The agent slot is occupied regardless of whether we can render an
issue link.
- Force `offline` for archived agents so they appear in the list
but never look like they're still on duty, matching the RFC
decision in MUL-2319.
- Include `workspaceKeys.squads` in the post-reconnect /
workspace-switch bulk invalidation so members-status recovers
after a disconnect during which task/runtime events were missed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The skill name Input on the detail editor uses `bg-transparent px-0`
to render as flush, chrome-less text. The base Input component also
applies `dark:bg-input/30`, which Tailwind keeps because it lives in
the `dark:` variant. In dark mode this exposes a 30% white fill that
appears flush against the text — looking like missing left padding.
Add `dark:bg-transparent` to the className so the override wins in
both color modes.
On desktop, localDaemonId is fetched async, so on first paint the only
machines available are remotes — the existing auto-select picks the
first remote, then sticks because subsequent renders see selectedMachineId
still in the list. Result: the local Mac never gets the default focus
even though it sorts first.
Re-evaluate the default on every machines change, preferring the local
section. Honor a user pick once it's been made.
Co-authored-by: multica-agent <github@multica.ai>