658 Commits

Author SHA1 Message Date
Bohan Jiang
bbe73ade8b feat(desktop): dock unread badge + focus-gated inbox notifications (#1445)
* feat(desktop): dock unread badge + focus-gated inbox notifications

Wire two OS-level integrations for inbox activity. Both degrade cleanly on
web and unsupported platforms.

- Unread badge on the macOS dock / Linux Unity launcher. Derived from the
  same inbox list the UI renders, deduplicated per issue, capped as "99+"
  on macOS via `app.dock.setBadge` (setBadgeCount truncates at 99). New
  `useInboxUnreadCount` hook (core/inbox) + `useDesktopUnreadBadge`
  (views/platform) keep renderer and main in sync via a `badge:set` IPC.
- Native OS notification on `inbox:new`, fired from the renderer only when
  `document.hasFocus()` is false — in-focus feedback is the existing inbox
  sidebar's unread styling, so we don't fight macOS's deliberate foreground
  suppression. Clicking the banner focuses the main window and navigates
  to `/inbox?issue=<key>` via the shared `multica:navigate` bus.

Refactors `inbox-page.tsx` to read the unread count through the new hook
(was a per-render inline filter).

* fix(desktop): pin notification routing to source workspace + mark read on URL select

Two bugs GPT-Boy caught on PR #1445:

1. A notification from workspace A used `getCurrentSlug()` at click time,
   so if the user switched to workspace B before clicking the banner (macOS
   Notification Center persists banners), routing landed on `/B/inbox?issue=<A key>`
   and 404'd. Fix: round-trip the emit-time `slug` through the IPC payload
   and use it in the click handler.
2. Notification-click navigation set the URL param but never fired the
   mark-read mutation (only InboxPage's click-handler did). The row stayed
   unread and the dock badge didn't decrement. Fix: move the mark-read
   logic from handleSelect into a useEffect keyed on the selected item —
   it now covers both click-to-select and URL-param-select.

IPC payload gains `slug` and `itemId`; preload types + main handler + the
desktop bridge are updated to match.
2026-04-28 17:33:48 +08:00
devv-eve
1845eaf42c fix: update kiro runtime icon (#1787)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
2026-04-28 17:21:30 +08:00
LinYushen
c366cf2ba1 feat(agent): add Kiro CLI ACP runtime (#1780)
* feat(agent): add kiro cli acp runtime

* fix(agent): align kiro acp prompt and notifications

* chore(agent): clarify kiro acp args compatibility
2026-04-28 17:03:46 +08:00
LinYushen
fae108ebdc fix: refresh mention issue search results 2026-04-28 16:54:41 +08:00
Bohan Jiang
0236e409e4 feat(issues): client-side label filter on the issues list (#1782)
Adds a Label submenu to the workspace issues filter dropdown, backed by
labelFilters in the shared issue view store. The filter is OR'd within
itself (issue matches if it carries any of the selected labels) and
AND'd with the existing status / priority / assignee / creator /
project dimensions, mirroring the multi-select semantics already in
place. Each label row renders via LabelChip for color parity with the
sidebar picker, and each row's count comes from the same
useIssueCounts pass that drives the other filter chips.

Filtering stays client-side, consistent with all other filters today.
The pagination caveat is a known limitation we'll revisit if real
workspaces start hitting it; this PR intentionally does not change the
fetch path.
2026-04-28 16:47:33 +08:00
Naiyuan Qing
b2fb39ed21 refactor(issues): flatten status group headers in list/board (#1783)
Drop the filled status chip (bg-warning/text-white etc.) from the
list/board column headers — StatusIcon already carries the semantic
color, so the chip duplicated it on the text background, and the
bg-muted variants were nearly invisible against the muted/40 row
background. Wrap the shared icon + label + count in a new StatusHeading
component used by both list-view and board-column.

Remove the now-unused badgeBg/badgeText fields from STATUS_CONFIG.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:45:26 +08:00
Bohan Jiang
abd69890a8 Revert "feat(issues): server-side filters incl. label, fixing pagination drop…" (#1779)
This reverts commit 246fcd4ce4.
2026-04-28 16:29:42 +08:00
Bohan Jiang
246fcd4ce4 feat(issues): server-side filters incl. label, fixing pagination drops (#1776)
* feat(issues): server-side label + filter querying for issue list

Extends GET /api/issues with label_ids, priorities, creator_ids,
project_ids, include_no_assignee, and include_no_project params, and
moves the existing single-value filters onto array-form. Each filter
becomes part of the SQL WHERE clause so paginated buckets reflect the
user's selection — fixes the bug where client-side filtering hid
matches sitting past the first page (#1491).

CLI gains a repeatable --label flag; legacy --priority/--assignee/
--project keep working via the single-value compatibility paths.

* feat(issues): drive workspace + my-issues filters from the server

issueListOptions and myIssueListOptions now key the React Query cache
on a normalized filter object, so each filter combination has its own
cache entry and a filter change re-fetches with the wire-shape filter
applied server-side. Drops the client-side filterIssues step on the
issues page, my-issues page, and project detail — that step silently
hid matches that lived past the first paginated page (#1491).

Adds a Label submenu to the workspace issues filter dropdown, plus
labelFilters in the view store. Mutations and ws-updaters fan their
optimistic patches across every filter-keyed list cache via
qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's
mention-suggestion reads from any matching list cache for instant
first paint regardless of which filter is active.

* fix(issues): route Members/Agents scope through server-side filter

The Members/Agents scope tabs on the workspace issues page were still
narrowing client-side via `assignee_type === 'member'`. That hits the
exact pagination-blind bug this PR is meant to fix: if the first 50
issues per status don't include the right assignee type, the tab
shows "No issues" while later pages have matches.

Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues /
CountIssues, threads it through the API client, normalizer and view
filter, and maps the scope tab to it. Each scope now keys its own
list cache and refetches with the correct first page.

Also disables the My Issues "My Agents" query when the user owns no
agents — `assignee_ids: []` was getting dropped by both the API client
and the query-key normalizer, so the request went out unfiltered and
surfaced unrelated issues under "My Agents".
2026-04-28 16:13:56 +08:00
Bohan Jiang
b8f661e006 feat(create-issue): default assignee to last-selected value (#1774)
The create-issue modal now remembers the assignee picked at submit
time and prefills the picker with that value when the modal next
opens. Implemented by tracking lastAssigneeType/Id alongside the
draft and seeding clearDraft's reset with those values.
2026-04-28 15:11:10 +08:00
devv-eve
c381d59c7a fix: preserve authored markdown links during linkify (#1761)
Co-authored-by: Eve <eve@multica.ai>
2026-04-28 08:57:15 +08:00
Bohan Jiang
1292ecf71b fix(labels): apply label attach optimistically (#1746)
* fix(labels): apply attach optimistically so chips render before round-trip

Attach went through onSuccess only, so users waited for the server
before seeing the new chip — out of step with detach (already optimistic)
and with status/assignee/priority via useUpdateIssue. Mirror the detach
pattern: snapshot the byIssue cache, look up the full label from the
workspace list cache, patch byIssue + the issue list/detail caches via
onIssueLabelsChanged in onMutate, and roll back on error. onSuccess and
onSettled keep the existing reconcile behavior.

* fix(labels): only patch attach when prev label set is known

GPT-Boy's review caught a corruption case: when byIssue cache was
unpopulated (user clicked before issueLabelsOptions resolved), the
optimistic patch fell back to an empty prev.labels, then mirrored
[label] into issue list/detail via onIssueLabelsChanged — wiping any
denormalized labels already on the issue. Worse, onError only restored
byIssue when ctx.prev existed, so the wipe persisted on failure.

Match useDetachLabel's invariant: skip the optimistic patch unless prev
is in cache. The chip will wait for the round-trip in the rare race
window, but caches stay consistent and rollback always works.
2026-04-27 18:24:40 +08:00
Bohan Jiang
d14265de2a fix(comments): preserve newlines from agent CLI writes (#1744)
* fix(comments): preserve newlines from agent CLI writes

Agents (e.g. Codex) routinely emit `multica issue comment add --content
"para1\n\npara2"` because Python/JSON-style string literals are their
default. Bash does not expand `\n` inside double quotes, so the literal
4-char sequence flowed through the CLI into the database and rendered
as text in the issue panel — comments came out as one wall of prose.

Three coordinated fixes so the platform behavior no longer depends on
whether a given model has strong bash-quoting intuition:

- CLI: decode `\n / \r / \t / \\` in `--content` and `--description` for
  `issue create / update / comment add` (callers needing a literal
  backslash still have `--content-stdin`).
- Agent prompt: rewrite the comment-add example in the injected runtime
  config to require `--content-stdin` + HEREDOC for any multi-line body,
  and call out the same rule for `--description`. The previous wording
  flagged stdin only for "backticks, quotes", which models read as
  irrelevant to plain paragraphs.
- Renderer: add `remark-breaks` to the shared Markdown plugin chain so a
  bare `\n` becomes a visible line break instead of a CommonMark soft
  break — protects against models that emit single newlines for
  formatting.

Tests: pin the new CLI helper, and pin the runtime-config guidance so
the multi-line wording cannot decay back into a footnote.

* fix(comments): address review feedback on newline-rendering PR

- Cover the issue panel: ReadonlyContent (used by every comment card and
  the issue description) has its own react-markdown wiring; add
  remark-breaks there too so the renderer fix actually applies to the
  surface the bug was reported on, not just the chat panel. Pinned by
  ReadonlyContent line-break tests.
- Make the prompt's `--description` guidance executable: add
  `--description-stdin` to `issue create` / `issue update`, refactor
  comment-add to share a single `resolveTextFlag` helper, and have the
  injected runtime config name the real flag instead of an imaginary
  "stdin / a tempfile" path. Pinned by the runtime-config guidance test.
- Document the unescape contract on each affected flag's help text and
  pin the precise boundary in tests: `\n / \r / \t / \\` are decoded;
  `\d / \w / \s / \u / \0` and other unrecognised escapes pass through
  verbatim, so regex literals and Windows paths survive intact unless
  they embed a literal `\n` / `\r` / `\t`. Callers that need the literal
  sequence have `--content-stdin` / `--description-stdin` as the escape
  hatch.
2026-04-27 17:17:34 +08:00
Bohan Jiang
bf6509be96 fix(issues): show labels in my-issues view + place chips after title (#1743)
- my-issues page lost labels because myIssuesViewStore cherry-picked
  name/storage/partialize from viewStorePersistOptions and dropped the
  cardProperties-aware merge. Persisted snapshots predating the labels
  toggle had cardProperties.labels = undefined, falsy-shorting the chip
  render. Extracted mergeViewStatePersisted as a generic and wired it
  into both stores.
- list-row chips now render right after the title (with a small left
  margin for breathing room) instead of in the right-aligned cluster.
2026-04-27 16:50:13 +08:00
Bohan Jiang
6620997503 feat(issues): render labels on list/board with bulk server-side fetch (#1741)
* feat(issues): render labels on list/board with bulk server-side fetch

ListIssues / ListOpenIssues / GetIssue now bulk-fetch labels per response
via a new ListLabelsForIssues query so the client gets labels in a single
round-trip instead of N requests per visible issue. List-row and board-card
read issue.labels directly; an issue_labels:changed WS handler patches the
list and detail caches in place so chips stay live across tabs, and
attach/detach mutations mirror their result into the same caches for
immediate same-tab feedback.

Adds a "Labels" toggle to the card properties dropdown (defaults on).

* fix(issues): preserve cached labels and refresh on label edit/delete

Three fixes from gpt-boy's review of #1741:

1. IssueResponse.Labels was a non-omitempty slice, so paths that didn't
   load labels (UpdateIssue, batch updates, the issue:updated WS broadcast)
   serialized labels:null. onIssueUpdated then merged that null into the
   list/detail caches, wiping chips on every other tab whenever any non-
   label field changed. Switched to *[]LabelResponse + omitempty: nil =
   field absent (client merge keeps existing labels); non-nil (incl. empty
   slice) = authoritative.

2. issue.labels is a denormalized snapshot, but useUpdateLabel /
   useDeleteLabel and the WS label:* prefix only touched labelKeys, leaving
   stale chips in list/board after rename/recolor/delete. Mutations now
   also invalidate issueKeys.all(wsId), and the realtime refreshMap maps
   the label prefix to both labels and issues invalidation for cross-tab.

3. Persisted cardProperties from before this branch lacks the new `labels`
   key. Render fell back to `?? true` but the dropdown switch read it raw
   and showed unchecked. Added a custom Zustand merge that deep-merges
   cardProperties so newly added toggles inherit defaults for existing
   users; dropped the `?? true` fallbacks now that the store guarantees
   the key.
2026-04-27 16:33:34 +08:00
Naiyuan Qing
e268ee3e71 refactor(views): centralize project icon rendering and fix nav active state (#1738)
Extract <ProjectIcon> with sm/md/lg sizes and a single 📁 fallback,
replacing 9 inline render sites that had drifted into 6 different
sizes and a mixed FolderKanban/emoji fallback.

Two visible fixes fall out of the centralization:
- ProjectPicker trigger now shows the selected project's icon (most
  visibly in the issue detail right Properties panel, where it had
  always been a generic FolderKanban).
- Sidebar parent nav (Projects, Issues, Settings, ...) now stays
  highlighted on child detail routes via a small isNavActive helper.
  Pinned items keep strict equality.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:42:56 +08:00
Ayman Alkurdi
e9d04ecfc1 feat(labels): ship issue labels (closes #1191) (#1233)
* feat(labels): add issue label CRUD + attach/detach handlers (#1191)

The issue_label and issue_to_label tables were scaffolded in 001_init.up.sql
but never wired to any code path. This commit ships the backend for #1191:

- Migration 048: adds created_at/updated_at timestamps + workspace-scoped
  case-insensitive unique index on label names
- sqlc queries for label CRUD + issue<->label attach/detach + batch list
  (ListLabelsByIssueIDs for board/list views)
- HTTP handlers: /api/labels CRUD, /api/issues/{id}/labels attach/detach
- Protocol events: label:{created,updated,deleted} + issue_labels:changed
- Handler tests covering CRUD, duplicate-name conflict, invalid-color,
  attach/detach idempotency, and cross-workspace isolation

* feat(cli): add label and issue label subcommands (#1191)

- multica label {list,get,create,update,delete}
- multica issue label {list,add,remove}

Both follow existing CLI conventions (JSON/table output, flag shapes)
and exercise the /api/labels endpoints shipped in the previous commit.

* feat(web): add labels UI — picker with inline create + management dialog (#1191)

Exposes the backend label feature to users via the existing issue-detail
sidebar.

- `@multica/core/types/label` — Label, CreateLabelRequest, UpdateLabelRequest,
  plus response envelopes
- `@multica/core/api/client` — 8 methods for label CRUD and issue↔label
  attach/detach
- `@multica/core/labels` — labelKeys, queryOptions, and mutation hooks with
  optimistic updates (matches the project/ module layout)
- WS event type literals extended for label:{created,updated,deleted} and
  issue_labels:changed

- `views/labels/label-chip.tsx` — colored pill; uses relative luminance
  (ITU-R BT.601) to pick #111827 or #f9fafb text so chips stay readable on
  both pastel and saturated backgrounds
- `views/issues/components/pickers/label-picker.tsx`
  - Multi-select combobox in the issue sidebar
  - When 0 labels: "Add label" trigger
  - When 1+ labels: the chips themselves are the trigger; × on each chip
    detaches without opening the picker
  - Inline create: typing a new name + Enter creates with a hash-derived
    color and attaches in one motion (matches Linear/GitHub)
  - "Manage labels…" footer opens a dialog containing the full workspace
    panel — users never leave the issue context to rename/recolor/delete
- `views/issues/components/labels-panel.tsx` — workspace labels manager.
  Single-row create form (color swatch + name + Add button). Each label
  row supports inline rename + recolor + delete (with confirm dialog).
  Color input uses the browser's native picker for full-gamut access —
  no preset palette clutter.

- `PropRow label="Labels"` added to the issue-detail sidebar below Project

Labels are issue metadata everyone uses — not admin configuration.
Putting them in Settings next to destructive workspace actions misframed
them; adding a top-level nav entry or a sibling tab to the Issues page
added surface area that wasn't earning its keep for a feature users
touch occasionally. Keeping management in a dialog launched from the
picker itself keeps users in their issue context and matches how GitHub
handles label editing from the label selector.
2026-04-27 14:23:42 +08:00
Jiayuan Zhang
04882c2201 feat(labs): Add labs settings tab (#1732) 2026-04-27 13:46:25 +08:00
Muhammadrizo
29122cc18b feat(sidebar): add dot to show the user about new invintation (#1711) 2026-04-27 11:41:03 +08:00
Black
04f813a70f fix PR 1573 follow-up colors (#1699) 2026-04-26 11:14:40 +08:00
Bohan Jiang
101601a4c3 fix(settings): render invite role label via roleConfig in members tab (#1693)
The invite-member role Select rendered the raw value ("member"/"admin")
in the trigger because Base UI's SelectValue defaults to the value, not
the item text. PR #1672 worked around it with `className="capitalize"`,
but this file already owns a roleConfig map with proper labels and the
codebase has an established render-prop pattern for SelectValue (see
trigger-config.tsx and runtime-local-skill-import-panel.tsx).

Use roleConfig[inviteRole].label inside SelectValue and reuse the same
labels for SelectItem children. Single source of truth for role display
names; future role additions or i18n won't depend on CSS capitalize.
2026-04-26 09:43:35 +08:00
Naiyuan Qing
c3ae212b40 fix(markdown): treat CJK full-width punctuation as URL boundary (#1630)
linkify-it only recognizes ASCII characters as URL boundaries. In Chinese
or Japanese text a URL followed by "。" (or any other full-width
punctuation) was greedily swallowed into the URL along with everything up
to the next whitespace, producing hrefs like
`https://.../pull/1623。merge` that 404 when clicked.

Truncate the detected URL at the first CJK full-width punctuation
character and re-scan the tail, so adjacent URLs separated only by
full-width punctuation are still each linked individually. The
terminator character set mirrors the fix applied in mattermost/marked#22.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:47:47 +08:00
Naiyuan Qing
71b2032174 feat(skills): restore page description, link to docs, polish intro layout (#1618)
* feat(skills): restore page description, link to docs, polish intro layout

The previous card-layout refactor (#1614) dropped the page-top
description entirely; without it the page jumps straight from the
PageHeader to a brand-colored banner that explains *how sharing works*,
with nothing answering "what IS a skill?". Bring the description back,
add a docs entry point, and tighten the visual hierarchy so the intro
block reads as one coherent unit above the table card.

- Restore a one-line description as the page's primary intro:
  "Instructions any agent in this workspace can use." — uses "any agent
  ... can use" (capability, not factual usage) since skills must be
  manually attached to take effect.
- Add an inline "Learn more about Skills →" link mirroring the
  onboarding docs-link pattern (muted underline, new tab) — opens
  https://multica.ai/docs/skills.
- Visual hierarchy: description is text-base + text-foreground (primary),
  link is text-xs + text-muted-foreground (auxiliary). Same line, eye
  follows weight order.
- Banner padding bumped from px-3 py-2 to px-4 py-3 so it breathes and
  its inner text lands at the same x as the table content.
- Wrap description + banner in a shared `pl-4 space-y-3` so they read as
  one intro block, indented to align with the table card's content.
- Loading skeleton updated to mirror the new structure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): keep docs link underline subtle, only animate text color on hover

The underline was inheriting text-decoration-color from the link's text,
so when hover bumped the text from muted to foreground the underline
got darker too — making the link feel more prominent on hover than at
rest, the opposite of what we want for a tertiary docs link.

Pin decoration-color to muted-foreground/30 explicitly so it stays
faint regardless of hover state. Only the text color transitions; the
underline stays as a constant low-key marker that the element is a link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:35:33 +08:00
Naiyuan Qing
f7fe0829f2 refactor(skills): wrap list as card, use shared PageHeader, add scroll fade (#1614)
The skills page rolled its own HeroHeader instead of the shared PageHeader,
which meant no mobile sidebar trigger and visual drift from other list
pages. The table was also edge-to-edge inside the dashboard container, so
it felt "naked" compared to the rest of the product.

- Replace custom HeroHeader with shared PageHeader (gives mobile hamburger
  and h-12 chrome for free); move "New skill" into the PageHeader as the
  page-level action.
- Keep search + scope filters in a toolbar, but move that toolbar *inside*
  a bordered, rounded card together with the table, so the whole unit
  reads as a single scrollable surface with internal padding.
- Use the existing useScrollFade hook on the row list so the top/bottom
  edges fade while scrolling.
- Drop `divide-y` in favor of `border-b` per row — divide-y leaves the
  last row without a bottom rule, which looks unfinished when only a
  couple of skills exist and the scroll area is taller than the content.
- Drop the redundant description paragraph from the old hero; keep the
  "Shared with your workspace" banner above the card since it carries
  non-obvious UX context.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:04:09 +08:00
Naiyuan Qing
c7e725ef66 feat: surface docs from onboarding + landing, unify Autopilot naming (#1613)
* docs(autopilot): rename Routines → Autopilots to match product UI

Unify naming between docs and product. Sidebar label, URL route,
CLI command, and onboarding copy all call this feature "Autopilot";
the docs were the only surface that diverged. Aligning the docs to
the product (rather than the reverse) because the 830+ code-side
references would be a much larger rename to propagate.

- Rename routines.mdx / routines.zh.mdx → autopilots.mdx / autopilots.zh.mdx
- Update meta.json / meta.zh.json index entries (routines → autopilots)
- Drop the reconciliation note ("docs say Routines, CLI says autopilot")
  that shipped in the original routines.mdx and the cli.mdx section header
- Update cross-references in cli, how-multica-works, tasks,
  assigning-issues, chat, mentioning-agents, daemon-runtimes (EN + ZH)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(onboarding): link to docs from key steps and starter tasks

Users who want to dig deeper now have a next hop from inside the flow
instead of having to dig through the help menu. Placed as secondary
links (muted, underline-offset-4) so they don't pull focus from the
primary CTA on each step.

Placement — one link per surface, placed in secondary regions:
- Welcome: "Learn how Multica works" below the subhead
- Questionnaire: "Learn how agents work" in the Why-we-ask aside
- Runtime aside (shared by desktop + web): "Learn about runtimes"
- Agent step: "Creating your first agent" in the About-agents aside
- StarterContentPrompt dialog: "Learn how Multica works"

Starter tasks (content/starter-content-templates.ts): added a single
"Learn about X" tail link per task, only on first occurrence of each
concept within a branch. 8 links on the agent-guided branch + 8 on
the self-serve branch + 1 on the welcome issue header (17 total).

URL scheme: absolute https://multica.ai/docs/{slug} throughout —
absolute so desktop (Electron) opens them in the system browser, and
the /en prefix is omitted because the docs middleware redirects it
away (English is the default, Chinese is /zh/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(landing): add docs link to footer and how-it-works section

Docs were previously reachable only from the in-app help menu. Landing
now surfaces them in two places, both locale-aware (/docs for English,
/docs/zh for Chinese):

- Footer Resources group: Documentation link was pointing at the
  GitHub repo; replaced with the real docs URL
- How-It-Works section CTA row: added "Read the docs" between the
  primary CTA and the GitHub link, same ghost styling

Locale resolution: href is picked per-render based on the landing's
current locale (cookie-driven via useLocale). The docs app itself
does not auto-detect language, so we must pick the right path
explicitly when emitting the link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(onboarding): clean up Autopilot rename leftovers and link formatting

- comments.mdx: "not routine updates" → "not day-to-day updates"
  (adjectival holdover now that the feature is renamed Autopilot;
  zeroes out remaining "routine" mentions in user-facing docs)
- starter-content-templates.ts: move the arrow inside the markdown
  link — "[text →](url)" instead of "→ [text](url)" — so the arrow
  is part of the clickable region. 17 occurrences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(onboarding): drop docs link from welcome screen and starter-content dialog

"Learn how Multica works" was showing up too often in the first two
screens users see. Keep the link in the post-import welcome issue
header (where users actually have time to explore); remove it from
the two earlier surfaces where it competes with the primary CTA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:27:53 +08:00
Naiyuan Qing
fe84e29b64 fix(ui): stop menu hover from overriding icon colors (#1612)
Menu primitives (context/dropdown/menubar/select/command) had rules like
`focus:**:text-accent-foreground` and `*:[svg]:text-destructive` that forced
descendant svg colors on focus, overriding icons that set their own color
(e.g. StatusIcon's `text-warning`). Remove them so icon color comes from
inheritance only: colored icons keep their color on hover, uncolored icons
still inherit the item's focus/destructive color as before.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:26:58 +08:00
Naiyuan Qing
4f40f70ea7 fix(skills): remove double-flicker on CreateSkillDialog close (#1610)
CreateSkillDialog used a controlled \`open\` prop while staying mounted,
so closing meant a data-open → data-closed flip on the already-mounted
Popup plus a tail re-render from \`useEffect([open])\` resetting \`method\`.
Visible as a double-blink: first the close animation, then a second
fade when the reset effect fired.

Align with the CreateIssue / CreateProject pattern: parent conditionally
renders the dialog and \`<Dialog open>\` is hard-coded. Close now unmounts
the component and Base UI's Portal owns the single exit animation. The
per-open method reset becomes unnecessary — fresh mount, fresh state.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:24:50 +08:00
Naiyuan Qing
7067d8f125 refactor(skills): redesign list page and add skill detail page (#1607)
* feat(core): add skill detail path and query helpers

- paths.workspace(slug).skillDetail(id) → /:slug/skills/:id
- skillDetailOptions(wsId, skillId) for fetching a single skill
- selectSkillAssignments(agents) folds the cached agent list into
  Map<skillId, Agent[]>; returns a stable reference so consumers can
  memoize against agent-array identity without re-rendering on unrelated
  agent updates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(views): add cross-platform openExternal helper

On Electron, route through window.desktopAPI.openExternal so the
http/https-only guard in the main process kicks in — direct window.open
inside Electron opens a new renderer window instead of handing the URL
to the OS shell. On web, fall back to window.open with noopener+noreferrer.
SSR-safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): extract edit-permission hook and origin helper

- use-can-edit-skill: mirrors the server's rule (admin/owner ∨ creator)
  so the UI can hide/disable actions instead of waiting for a 403. Takes
  wsId explicitly per the repo rule for workspace-aware hooks.
- lib/origin: discriminated view over Skill.config.origin (manual /
  runtime_local / clawhub / skills_sh) so consumers don't spread JSONB
  parsing across the UI tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): rewrite skills list page and collapse import UI

- SkillsPage rewritten: new hero header, single table layout with
  columns (Name / Used by / Source · Added by / Updated), agent avatar
  stack per skill, filter tabs aligned with Issues/MyIssues header
  (Button variant=outline + Tooltip + bg-accent active state).
- CreateSkillDialog: dedicated dialog for the manual/import entry
  points, replaces the inline row-triggered dialog.
- runtime-local import: dialog variant deleted; panel is now the single
  entry point, embeddable inside CreateSkillDialog. Panel covered by a
  new test.
- Deleted runtime-local-skill-row (no longer needed — row rendering
  lives in SkillsPage directly) and the old skills-page.test.tsx
  (structure diverged beyond salvaging; will be re-added alongside the
  detail-page tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): add skill detail page and wire routes on web and desktop

- SkillDetailPage: dedicated view for a single skill (name, description,
  origin, assignments, file listing). Uses skillDetailOptions and the
  new origin / use-can-edit-skill helpers.
- apps/web: /:workspaceSlug/skills/:id Next.js route.
- apps/desktop: /:slug/skills/:id added to the memory router under
  WorkspaceRouteLayout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(skills): bump runtime-local-skill-import-panel timeouts for CI

The test chains a five-step async cascade (runtime list → setSelectedRuntimeId
effect → skills query → auto-select effect → row render). Comfortable on
local (~600ms) but tight against RTL's 1 s default on CI where jsdom +
Vitest import takes ~100s. Bump findByText and the two waitFor calls to
5 s each — no production behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:51:58 +08:00
devv-eve
9ed1fa95fc feat(server): add readiness health endpoints (#1605)
* feat(server): add readiness health endpoints

* fix(server): cache readiness checks

* fix(server): raise readiness cache ttl

---------

Co-authored-by: Eve <eve@multica.ai>
2026-04-24 13:50:24 +08:00
Naiyuan Qing
147fb2ee66 fix(autopilot): confirm before deleting autopilot or trigger (#1604)
Destructive actions in the autopilot detail page fired immediately on
click. Wrap "Delete autopilot" and per-trigger delete with AlertDialog
confirmation, matching the existing issue-delete pattern.

Also fix a latent bug in trigger deletion where the success toast was
shown synchronously after mutate(), so failures still reported success —
switch to mutateAsync + try/catch.
2026-04-24 13:11:52 +08:00
Naiyuan Qing
5bab95ad26 fix(issues): unify board card hover and active visual (#1603)
Hover and popup-open states now share the same bg-accent + border-accent
treatment. Drop the shadow-md hover (invisible in dark mode) and the
multi-property transition in favor of a single transition-colors.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:55:30 +08:00
Naiyuan Qing
0bd6ba9354 fix(issues): cleaner board card hover with shadow elevation (#1600)
Replace translucent tinted hover (border-accent/50 + bg-accent/20) with
a single-dimension shadow lift. The previous overlay was visually weak
because --accent is nearly identical to --card, so a 20% tint rendered
as almost no change. Active (popup-open) state now uses solid bg-accent
so hover and active are distinguished by different dimensions —
elevation vs color — instead of competing on the same axis.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:15:07 +08:00
Naiyuan Qing
40cea8454d feat(autopilot): redesign modal — simpler schema, consistent schedule UI (#1595)
Drop priority and project_id from autopilot. project_id was never exposed
in the UI and priority duplicated the agent's own task queue priority.

Redesign the create/edit modal as a Runbook (left) + Configuration (right)
layout. Rework the Schedule section around a single visual shell so every
picker aligns pixel-for-pixel on the same row:

- TimeInput (new): segmented HH:MM control adapted from openstatusHQ/time-picker,
  driven by keyboard (ArrowUp/Down to step, ArrowLeft/Right to jump segment,
  digit typing with a 2s two-digit window). Replaces <input type="time">,
  whose native UI broke the design system. Supports a minuteOnly variant
  for hourly schedules.
- TimezonePicker (new): searchable Popover with a fixed-width left check
  slot so rows stay aligned and GMT offsets never collide with the selected
  indicator.
- Runbook editor now lives in a bordered card, giving the placeholder an
  input surface instead of bare document flow.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:05:33 +08:00
Naiyuan Qing
d54daa62c5 feat(issues): right-click context menu + unified issue actions (#1594)
* feat(issues): add right-click context menu on list rows and board cards

Extract the detail page's ⋯ dropdown (~180 lines of inline JSX) into a
shared `useIssueActions` hook plus two thin wrappers so the same action
set (status / priority / assignee / due date / sub-issue ops / pin / copy
link / delete) can be mounted as both a DropdownMenu and a Base UI
ContextMenu. Right-click on any list row or board card now opens the
full action menu without entering the detail page.

Shell-level modals replace the detail-page-local state for set-parent /
add-child / delete-confirm / backlog-agent-hint, so any trigger (detail
page, list, board) can open them through `useModalStore`. Detail page
detects its own deletion via a query-transition effect, avoiding the
need to smuggle callbacks through the store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(issues): hover and active styling on list rows and board cards

Mirror the sidebar's same-color/different-intensity pattern for the new
right-click context menu states. Base UI adds `data-popup-open` to the
ContextMenuTrigger when the menu is open; `hover:not-data-[popup-open]`
suppresses hover feedback on the already-active item.

List rows apply the pattern directly to background color (`accent/60`
hover, `accent` active). Board cards additionally modulate the card's
border and a lighter background tint (`accent/20` hover, `accent/40`
active) so the card's own bg/border/shadow identity stays intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(modals): show target issue banner in SetParent/AddChild pickers

When triggered from an issue's action menu, the IssuePickerModal now
displays a banner at the top showing "Setting parent of" / "Adding
sub-issue to" followed by the originating issue's status, identifier,
and title. Previously the operation target was only implied by the
modal's sr-only title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(modals): create-issue gains ⋯ overflow menu with parent issue linkage

Add a dropdown-menu with "Set parent issue..." / "Remove parent" at the
end of the property pill row. The ⋯ button is always the last DOM child
of the row so it stays at the tail even when the row wraps to multiple
lines. Menu state reflects current selection — unset shows a single
"Set parent…" entry, set shows the current identifier plus a separate
Remove option.

When a parent is set (either via the new menu or via `data.parent_issue_id`
from a "Create sub-issue" trigger), a chip appears in the pill row
showing "Sub-issue of {identifier}" with the same click-to-change /
click-×-to-clear semantics. This replaces the old header breadcrumb
disclosure that was neither editable nor visible in the form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(issues): group relationship actions under "More" submenu

Nest Create sub-issue / Set parent issue / Add sub-issue inside a
`More >` submenu in the issue actions menu (both Dropdown and
Context variants). Top-level keeps Status/Priority/Assignee/Due date
category submenus plus Pin and Copy link; the relationship ops are
lower-frequency and will grow with future relation types (blocks,
duplicates, related) that fit the same category.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(modals): create-issue adds Add sub-issue with deferred linking

The create modal's ⋯ menu gains an "Add sub-issue..." entry that queues
existing issues as children of the new one. Picked issues appear as
chips in the pill row (downward arrow, distinct from the upward parent
chip), each individually removable.

Linking is deferred because the new issue's ID doesn't exist at pick
time. Once createIssueMutation resolves, we run updateIssueMutation
for every queued child in parallel and surface any partial failures
via toast — the new issue itself is already committed and never rolls
back. Parent and child pickers exclude each other so a single issue
can't occupy both relations simultaneously.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* polish(issues): add MoreHorizontal icon to "More" submenu trigger

The "More" label was visually misaligned because every other top-level
entry has a leading icon. Use MoreHorizontal (same icon as the outer ⋯
trigger — semantically "more options, nested") and drop the `inset`
padding hack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(modals): drop target-issue banner from IssuePickerModal

The banner sat directly above the search input and rendered the target
issue with bolder styling than the "Setting parent of" / "Adding sub-issue
to" caption, which made it read like a pre-selected search result rather
than a context label. Users opening the modal from a menu item already
carry the context, so the extra chrome was redundant.

Remove the contextIssue / contextLabel API from IssuePickerModal and
drop the now-unused issueDetailOptions query in SetParentIssueModal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* polish(modals): exclude current parent from create-issue parent picker

Re-opening the parent picker to change the already-set parent used to
show that parent in the results — picking it was a silent no-op. Mirror
the child picker's exclude-list construction so the current parent is
always filtered out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:48:46 +08:00
Jiayuan Zhang
71cc646951 fix(chat): prevent UI flicker when streaming response finalizes (#1583)
The live timeline was rendered in a separate <div> from the persisted
messages list. When the streamed task finished and its ChatMessage
landed, the live <div> unmounted and a new <MessageBubble> mounted —
two different DOM elements showing the same content. useAutoScroll's
ResizeObserver + MutationObserver fired on both the unmount and the
mount, causing the visible jump-then-re-render.

Merge the two paths: inject a synthetic assistant message with the
pending task_id while streaming, and key every assistant bubble by
task_id. When the real message arrives (same task_id), React preserves
the DOM element across the invalidate → refetch window — no remount,
no double scroll, no flicker.

Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
2026-04-24 02:01:12 +08:00
Jiayuan Zhang
bb767e0ea6 fix(chat): prevent chatbox jump when sending first message (#1582)
The ChatInput wrapper toggled between pb-8 (empty state) and pb-4
(has messages), causing a 16px vertical jump the moment hasMessages
flipped. EmptyState already centers itself inside flex-1, so the
extra padding wasn't needed — collapse to a single pb-4.

Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
2026-04-24 02:00:12 +08:00
Jiayuan Zhang
35aca57939 feat(chat): Chat V2 — sidebar entry + main-area page (#1580)
* feat(chat): Chat V2 — sidebar entry + main-area page

Replace the floating drawer + FAB with a first-class workspace route
`/:slug/chat`. Sidebar gets a single `Chat` entry under Inbox with an
unread dot; session history lives inside the Chat tab via a popover
rather than leaking into the global sidebar (keeps Multica's "nouns in
the nav" semantic — Inbox / Issues / Projects are work objects, Chat is
a tool).

- Add `paths.workspace(slug).chat()` + update link-handler route set.
- New `ChatPage` view with PageHeader, history popover, centered
  messages/composer column, and empty-state starter prompts.
- Delete `ChatWindow`, `ChatFab`, resize helpers, and standalone
  `ChatSessionHistory` (history now embedded in the popover).
- Drop `isOpen`/`toggle`/`showHistory`/resize fields from `useChatStore`
  — the page is a route now, not an overlay.
- Wire the new `/chat` route on web (App Router) and desktop
  (react-router + tab-store icon mapping).

Addresses MUL-1322.

* fix(chat): align composer width with message column

The ChatPage wrapper added px-4 on top of ChatInput's own px-5, making
the composer 32px narrower than the messages column. Drop the outer
px-4 so both share the same max-w-3xl outer + px-5 inner padding
provided by ChatMessageList / ChatInput.

* fix(chat): taller default composer (~3 lines visible, 8 max)

min-h 4rem → 7rem, max-h 10rem → 15rem. Empty state previously
showed only 1 text row after pb-9 for the action bar; raise the
floor so there's visible writing room and lift the ceiling so a
longer draft can grow before scrolling kicks in.

* fix(chat): restore anchor + in-flight indicator + cold-start session restore

Three issues surfaced by review:

1. ContextAnchorButton always disabled on /:slug/chat — useRouteAnchorCandidate
   only matches issue/project/inbox pathnames, so moving chat to its own route
   dropped 'bring the page I was on into the conversation'. Track the last
   anchor-eligible location globally (new useAnchorTracker mounted in AppSidebar
   + lastAnchorLocation on useChatStore) and substitute it when on /chat.

2. No global 'Multica is working' cue after ChatFab deletion. Subscribe the
   sidebar Chat entry to pendingChatTasksOptions and swap the unread dot for a
   spinner while any chat task is in flight.

3. ChatPage restore effect latched didRestoreRef before the sessions query
   resolved, so cold-start direct nav to /chat landed on the empty state even
   when the server had an active session. Wait for isSuccess before locking
   the ref.

* fix(chat): clear lastAnchorLocation on workspace rehydration

The pathname captured in workspace A would otherwise be reused against
workspace B's wsId, triggering a cross-workspace issue/project fetch
and silently leaking anchor context into chat messages.

---------

Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
2026-04-24 01:46:37 +08:00
Jiayuan Zhang
977b0c0558 feat(agents): show profile card on agent avatar hover (#1577)
* feat(agents): show profile card on agent avatar hover

Hovering an agent avatar now opens a preview card with name, status,
runtime mode + connectivity, model, skills, and owner. Wired through
the shared ActorAvatar wrapper so every render site gets it; opt-out
via disableHoverCard in pickers and the agent's own detail header
where the card would be redundant or interfere with click selection.

* fix(agents): keyboard-focusable hover card + opt out on settings avatar

- Make the agent profile-card hover trigger focusable (tabIndex=0 with
  visible focus ring), so keyboard users can open the card. Drops
  cursor-default so the trigger inherits the parent control's cursor
  instead of fighting it.
- Disable the hover card on the agent settings avatar — it's a
  click-to-upload target on the agent's own settings page, where the
  card would be redundant and the trigger conflicted with the upload
  affordance.

* fix(agents): scope hover-card tab stop to standalone avatars only

Detect a focusable ancestor (link/button/role=button/tabindex>=0) at
mount and only flip the agent profile-card trigger to tabIndex=0 when
none exists. Avatars rendered inside an existing focusable parent (issue
list rows wrapped in AppLink, button-style cards, etc.) keep the trigger
unfocusable so they don't add redundant nested tab stops or bloat
keyboard navigation. Standalone avatars (e.g. comment author, issue
detail meta) remain keyboard-accessible with a focus-visible ring.

---------

Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
2026-04-24 00:53:55 +08:00
Black
17136742b9 fix(runtimes): fix dark mode chart visibility and invalid CSS color syntax (#1573)
All chart components used `hsl(var(--chart-X))` but `--chart-X` holds a
full oklch value, not bare HSL components — making the expression invalid
CSS. Browsers silently fell back to black, so bars/areas/heatmap cells were
invisible against the dark background.

- Replace `hsl(var(--chart-X))` with `var(--color-chart-X)` across all
  runtime chart components and the landing feature section
- Fix heatmap opacity using `color-mix(in oklch, ...)` instead of the
  invalid `hsl(var(--chart-3) / 0.3)` syntax; switch to foreground color
  so cells blend with the neutral theme in both light and dark mode
- Raise dark-mode chart-2 through chart-5 lightness values so they
  contrast clearly against the dark background

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 00:47:41 +08:00
Naiyuan Qing
044d1443b5 fix(issues): keep reply editor expand icon muted on focus (#1565)
The expand button relied on the parent row's inherited color, which
flipped to text-foreground via group-focus-within while the editor was
focused. The attach and submit buttons set text-muted-foreground on
themselves and stayed muted regardless of focus, so expand was the only
one changing color — inconsistent with the "default muted" convention
the other icon-buttons in this editor follow.

Give expand its own text-muted-foreground and drop the now-unused color
classes from the button row container.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:05:36 +08:00
Naiyuan Qing
502add4bd1 fix(issues): restore compact single-line reply editor, keep expand overlap fix (#1562)
#1558 fixed the expand button covering trailing text, but also collapsed
the reply editor's "empty = 1 line, has content = 2 lines" behavior by
making the button row a permanent flex sibling below the editor.

Restore the original absolute-positioned button row on both editors:

- comment-input: back to `pb-8` container + `absolute bottom-1 right-1.5`
  buttons (pre-#1558 layout; never had the overlap bug).
- reply-input: absolute buttons + `pb-7` gated on `!isEmpty || isExpanded`.
  Empty → single-line compact; any content → two-row layout with buttons
  below text (no overlap by construction).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:37:36 +08:00
Naiyuan Qing
e994d77982 feat(help): mark external links with arrow, move Feedback last (#1560)
Add an ArrowUpRight glyph next to Docs and Change log to signal they
open externally, and reorder so Feedback (internal modal) sits at the
bottom.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:18:18 +08:00
Naiyuan Qing
efc08a1e37 fix(issues): stop expand button from covering text in comment/reply editors (MUL-1297) (#1558)
The comment and reply editors positioned their three trailing buttons
(expand, attach, submit) with `absolute` and relied on `pr-14` /
`pb-8` magic numbers to reserve space. The reserved 56px is smaller
than the actual 80px button row, so the leftmost button (expand)
visibly overlaps the trailing characters of a long line of text.

Restructure the button row as a normal flex sibling below the editor.
Text can no longer flow under the buttons, and the layout no longer
needs the `pr-14` hack, `pb-8` padding, or the ResizeObserver that
toggled `pb-7` when content overflowed.

Also align the expand button in comment-input with the reply-input
version (`h-6 w-6` + `h-3.5 w-3.5` icon) so the two entry points
match.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:38:04 +08:00
Bohan Jiang
6fd1255873 feat(runtimes): remove Test Connection / runtime ping feature (#1554)
* feat(runtimes): remove Test Connection / runtime ping feature

The Test Connection action invoked a real single-turn agent run to verify
runtime connectivity. In practice it was expensive (reuses none of the
normal task exec env, so it also gave misleading results) and low value —
daemon heartbeat + Online status already covers the "is the runtime
alive" question. Dropping the whole end-to-end probe path:

- deletes server handler and in-memory PingStore
- drops pending_ping from the heartbeat response and daemon poll loop
- removes daemon.handlePing, PendingPing, ReportPingResult
- removes the CLI `multica runtime ping` command
- removes the PingSection UI block and RuntimePing types / api methods

* docs: fix runtime CLI subcommand list in product-overview
2026-04-23 16:18:21 +08:00
Naiyuan Qing
6c72c71e3e feat(analytics): add onboarding_runtime_detected event on desktop Step 3 (#1553)
Answers "did the user have an AI CLI installed locally when they hit
Step 3" — currently unanswerable from the existing funnel because the
bundled daemon fails to register at all when zero CLIs are on PATH, so
`runtime_registered` is silent on that cohort. Splits the 40% of
`completion_path=runtime_skipped` into "had CLIs, skipped anyway" vs "no
CLIs available, had no choice" — the two cases need opposite product
fixes.

Fires once per Step 3 mount in `step-runtime-connect.tsx` (desktop
only), when the scanning phase resolves — either immediately on first
runtime registration or after the 5 s empty timeout. Reports
`runtime_count`, `online_count`, sorted `providers`, convenience
booleans (`has_claude` / `has_codex` / `has_cursor`), and `detect_ms`.
Also writes `has_any_cli` + `detected_cli_count` via `$set` as cohort
signals.

Not emitted from the web Step 3 (`step-platform-fork.tsx`) — web users
don't run the bundled daemon, so their runtime list can reflect
daemons on other machines and would corrupt the
"CLI installed locally" signal.

Refs MUL-1250.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:56:55 +08:00
Bohan Jiang
fae3afee79 fix(agents): drop auto-loading Local Runtime Skills section from Skills tab (#1551)
* fix(agents): drop auto-loading Local Runtime Skills section from Skills tab

Every visit to an agent's Skills tab fired POST
/api/runtimes/<id>/local-skills + a polling GET, which:

- Created noise on every tab open (the section was rarely the user's
  reason for entering the tab — they came in for workspace skills).
- Currently 404s under the dev backend's multi-replica deploy because
  the runtime-local-skills request store is in-process; the polling
  GET frequently lands on a different replica than the POST. The
  protocol fix is tracked separately; this PR just stops the
  unsolicited polling.

Removes the entire `Local Runtime Skills` inline section, the
`runtimeLocalSkillsOptions` query, and the per-skill Import dialog
mount on this tab. Users who want to import a local skill go through
the Skills page's `+ Add Skill` → `From Runtime` tab — the same flow
that handles all other skill creation, only triggered explicitly.

Top blue callout stays — still accurate: local runtime skills are
auto-available to the agent, importing creates an editable workspace
copy.

* test(agents): replace stale Local Runtime Skills assertion with negative case

The previous test required the inline section + auto-loading runtime
local skills query, both removed in this PR. Replace it with a
regression test that asserts the section is gone, the per-row import
button is gone, and the top informational callout still renders so we
know the tab body actually mounted.

Drops the now-unused @multica/core/runtimes mock; if a future change
re-introduces that import, the missing mock would surface immediately.
2026-04-23 15:47:29 +08:00
Naiyuan Qing
d6e7824ff1 feat(feedback): in-app feedback flow + Help launcher (#1546)
* feat(feedback): add in-app feedback flow and Help launcher

Replaces the duplicated bottom-sidebar user popover and "What's new" links
with a single Help menu (Docs / Feedback / Change log) pinned to the
sidebar footer. Feedback opens a rich-text modal that POSTs to a new
/api/feedback endpoint; submissions land in a dedicated feedback table
with per-user hourly rate limiting (10/hr) to deter spam without adding
middleware infrastructure. User identity (avatar + name + email) moves
into the workspace dropdown header so the sidebar is no longer visually
redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(feedback): harden submit path and cap request body

- Read editor markdown via ref at submit time instead of debounced state,
  so ⌘+Enter immediately after typing doesn't drop the last keystrokes.
- Block submission while images are still uploading; toast prompts the
  user to wait instead of silently sending markdown with blob: URLs
  that get stripped.
- Cap /api/feedback request body at 64 KiB via MaxBytesReader so an
  authenticated client can't bloat the metadata JSONB column with an
  oversized url field.
- Add Go handler tests covering happy path, empty-message rejection,
  and the hourly rate limit boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(analytics): instrument feedback funnel

Adds two events pairing frontend intent with backend conversion so we
can compute a completion rate for the in-app Feedback modal:

- `feedback_opened` (frontend) — fires once on FeedbackModal mount.
  Source is currently always "help_menu" but the type is a union so
  future entry points have to extend it explicitly. Workspace id is
  attached when present.
- `feedback_submitted` (backend) — fires from CreateFeedback after the
  DB insert succeeds and the hourly rate-limit check has passed.
  Message content itself is never sent to PostHog; the event carries
  a coarse length bucket (0-100 / 100-500 / 500-2000 / 2000+), an
  image-presence flag, and the client platform / version pulled from
  X-Client-* headers via middleware.ClientMetadataFromContext.

Affects no existing funnel; seeds a new Feedback funnel for product
triage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:35:55 +08:00
Mack
f2ba087f74 fix(editor): preserve nested ordered lists through readonly render (#1512)
Default @tiptap/markdown serializer emitted nested list items with 2-space indent, but CommonMark (remark-gfm) requires ≥3 spaces under a `1.` marker — so ReadonlyContent (autopilot detail / issue / comment) flattened nested ordered lists, with third-level items glued onto their parent line. Configure Markdown extension with indentation.size = 3.

Closes #1510
2026-04-23 07:08:19 +08:00
Bohan Jiang
6717db1fad feat(agents): surface task source on AgentTaskResponse + use it in Tasks tab (#1455)
Follow-up to #1453. That PR fixed the Tasks tab crash by filtering empty
issue_id out of the detail lookup and rendering a neutral "Task without
linked issue" label, but every issue-less task — chat-spawned or
autopilot-spawned — looked the same. The server already stores the
origin in `agent_task_queue.chat_session_id` / `autopilot_run_id`; only
the HTTP serializer was dropping them.

Server:
- `taskToResponse` now populates `ChatSessionID` and the new
  `AutopilotRunID` on `AgentTaskResponse`. Backward compatible: both
  omit when UUID is invalid, and existing clients ignore unknown
  fields.

Types:
- `AgentTask` (TS) gains `chat_session_id?` + `autopilot_run_id?` and a
  comment clarifying when `issue_id` is empty.

Tasks tab:
- Row label for issue-less tasks is picked from the populated source
  field: "Chat session" for chat tasks, "Autopilot run" for autopilot
  tasks, "Task without linked issue" as the neutral fallback. Rows stay
  inert (no anchor) in all three cases; existing issue-linked path is
  unchanged.

Tests:
- Two new regression tests assert the chat and autopilot labels render
  correctly and neither row becomes an anchor. Existing neutral-label
  test stays as the "neither source populated" case.
2026-04-22 19:26:57 +08:00
Naiyuan Qing
f84d216794 fix(views): restore issue-mention class on <a> for mention card (#1516)
PR #1502's IssueChip extraction moved the `issue-mention` class from the
outer <a> into IssueChip's inner <span>, breaking three consumers that
select on `<a>.issue-mention` directly:

- `.rich-text-editor a.issue-mention` underline-exemption in
  content-editor.css (stopped matching -> mentions in editor gained a
  spurious underline).
- `link-hover-card.tsx` classList check that suppresses the URL preview
  on issue mentions (stopped matching -> hover card wrongly pops up
  over mention chips).
- Tailwind Typography prose (`prose a { text-decoration: underline }`)
  covers a separate path — markdown bubbles in chat. prose's specificity
  (0,1,1) beats `.no-underline` (0,1,0), so `not-prose` is the right
  escape hatch on the AppLink.

Put `issue-mention` back on the <a> in both wrappers (IssueMentionCard
and the editor's MentionView), and add `not-prose` only to the markdown
wrapper. IssueChip's BASE_CLASS keeps `issue-mention` too (inert on the
span; removing it is a separate scope that needs a full consumer audit).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:13:14 +08:00
Naiyuan Qing
f6dd47c944 fix(chat): disable focus button on pages without an anchor (#1509)
The focus toggle was only disabled when focusMode was already ON
*and* the current page had no anchor. Off-state on the same page
stayed clickable — clicking turned it on, and the button instantly
greyed out, making the missing fourth state visible.

Decouple "clickable" from focusMode: the button is disabled whenever
the current page has no anchor, regardless of the persisted on/off
preference. Both the chip render (context-anchor.tsx:173) and send
path (chat-window.tsx:176) already guard on candidate presence, so
leaving focusMode=true on an unanchorable page has no side effects —
the preference is preserved for the next anchorable page.

Tooltip now reads "Nothing to share with Multica on this page"
whenever the button is disabled, regardless of focusMode.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:57:44 +08:00