The issue detail page breadcrumb showed both the issue identifier and
title (e.g. "MUL-1567 Title"), making the ID appear twice. Remove the
standalone identifier span so only the title is displayed.
ProseMirror's default clipboardTextSerializer uses Slice.textBetween,
which flattens every node to its inner text. Copying `## 你好` from the
editor only put `你好` on the clipboard's text/plain channel, so pasting
into VS Code, terminals, or messaging apps lost all Markdown markers.
Add a markdown-copy extension symmetric to the existing markdown-paste:
on copy/cut/drag, route the selected Slice through editor.markdown.serialize
to write the Markdown source. The text/html channel is left at ProseMirror's
default so pasting back into another ProseMirror editor still preserves
exact node structure via data-pm-slice.
Registered for both editable and readonly modes — users frequently copy
from rendered comments/issue descriptions.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(quick-create): bound dialog height + scroll editor when content overflows
Pasting a screenshot into the agent-create prompt expanded the editor
unbounded, which dragged DialogContent past the viewport since the agent
mode className had no max-height. Manual mode was unaffected because
manualDialogContentClass pins `!h-96`.
- Cap agent-mode DialogContent at `!max-h-[80vh]` (width stays
`!max-w-xl`); short prompts still render compact, tall content stops
at 80% of the viewport.
- Switch the editor wrapper to `flex-1 min-h-[140px] overflow-y-auto`
so it absorbs the remaining vertical space inside the now-bounded
DialogContent and scrolls internally instead of pushing the dialog.
* feat(quick-create): gate on daemon CLI version with pre-check + server enforcement
The agent-create flow depends on multica CLI behavior introduced in
v0.2.20 (URL attachment handling, no-retry semantics on
`multica issue create` failure — see PR #1851 / MUL-1496). Older
daemons either double-create issues on partial CLI failures or
mishandle pasted screenshot URLs. Per J's review on MUL-1496, gate
the flow at two layers — frontend pre-check for fast feedback,
server re-check as the trust boundary, both fail-closed on
missing/unparsable versions.
Server:
- New MinQuickCreateCLIVersion + CheckMinCLIVersion helper in
pkg/agent (with sentinel errors for missing vs too-old).
- QuickCreateIssue handler reads runtime metadata.cli_version and
returns a stable 422 { code: "daemon_version_unsupported",
current_version, min_version, runtime_id } before enqueuing.
- The check runs after the existing online + ownership validation,
so all rejections surface uniformly through the modal's existing
error path.
Frontend:
- New @multica/core/runtimes/cli-version with the min version
constant, parser, and runtime-metadata reader (tiny semver, no
new lib dep).
- AgentCreatePanel resolves the selected agent's runtime, runs the
same check, shows an inline amber notice below the agent picker
when missing/too old, and disables the Create button.
- Submit handler also catches the server's 422 (defensive race —
runtime can re-register between pre-check and submit) and
surfaces the same wording in the error row.
Switching to manual create remains a clean escape hatch — manual
mode doesn't talk to a daemon at all, so an outdated CLI doesn't
block the user from filing the issue.
A complete UX upgrade for chat sending → receiving → recovering.
* StatusPill replaces the orphan spinner — stage-aware copy
("Reading files · 12s", "Searching the web · 14s", "Typing · 24s"),
shimmer text, monotonic timer, derived effective status, > 60s
warning tone, > 5min cancel button.
* WS writethrough on task:queued / task:dispatch / task:cancelled so
pendingTask cache stays in sync with the daemon state machine without
invalidate-refetch latency. broadcastTaskDispatch now includes
chat_session_id when the task is for a chat session — the existing
payload only carried it on the generic task: events, leaving the pill
stuck at "Queued" until completion.
* Failure fallback — FailTask writes a chat_message tagged with
failure_reason (mirrors the issue path's system comment, gated on
retried==nil). Front-end renders an inline note ("Connection failed",
with a Show details collapsible) instead of the previous black hole.
* Elapsed timing — chat_message.elapsed_ms persists task.completed_at -
task.created_at on success/failure rows. UI shows "Replied in 38s" /
"Failed after 12s" beneath assistant bubbles. Format helper shared
between StatusPill and the persisted caption so the live timer and
final reading never disagree.
* Optimistic burst rebalanced — pendingTask seed + created_at moved
before the HTTP roundtrip so the pill appears the instant the user
hits send; handleStop is fire-and-forget so cancel feels immediate
(server confirmation arrives via task:cancelled WS).
* Presence integration — chat avatars use ActorAvatar (status dot +
hover card); OfflineBanner above the input on offline/unstable;
SessionDropdown shows per-row in-flight/unread pip plus a
cross-session aggregate pip on the closed trigger.
* Editor blur on send so the caret stops competing with the StatusPill
/ streaming reply for the user's attention.
* Chat panel isOpen now persists globally; defaults to OPEN for new
users (storage key absence) so the feature is discoverable. Existing
users' prior choice is respected.
* DB: migrations 062 (failure_reason) + 063 (elapsed_ms), both
ADD COLUMN NULL — fast, non-blocking, backwards compatible.
* WS: task:failed chat path now invalidates chatKeys.messages — fixes
a pre-existing bug where the failure bubble required a page refresh
to appear.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pasting a screenshot into the agent-create prompt expanded the editor
unbounded, which dragged DialogContent past the viewport since the agent
mode className had no max-height. Manual mode was unaffected because
manualDialogContentClass pins `!h-96`.
- Cap agent-mode DialogContent at `!max-h-[80vh]` (width stays
`!max-w-xl`); short prompts still render compact, tall content stops
at 80% of the viewport.
- Switch the editor wrapper to `flex-1 min-h-[140px] overflow-y-auto`
so it absorbs the remaining vertical space inside the now-bounded
DialogContent and scrolls internally instead of pushing the dialog.
* fix: polish quick-create UX (kind labeling, dark toast, placeholder)
Three small fixes shaken out from using the agent-create flow:
- AgentTaskResponse now carries a `kind` discriminator
("comment" | "autopilot" | "chat" | "quick_create" | "direct"), computed
from the existing FK shape with no extra DB access. The Activity row
uses it to label quick-create tasks as "Creating issue" instead of
falling through to the generic "Untracked" — once the agent finishes
and the new issue is linked, the row transitions to the normal
identifier+title display.
- Sonner Toaster reads `resolvedTheme` instead of `theme`, so toasts
follow the actual dark/light state. Forwarding "system" let sonner
pick its own answer from `prefers-color-scheme`, which in the Electron
renderer can disagree with next-themes' `html.dark` class — the toast
rendered light on a dark UI.
- Agent-create placeholder rephrased to a more conversational example
with a project reference: "let Bohan fix the inbox loading slowness
in the Web project". Drops the priority hint (priority isn't widely
used) and matches how people actually instruct the agent.
* fix(quick-create): link new issue back to task on completion
Addresses the review on PR #1831: completed quick-create tasks were
left with issue_id=NULL forever, so the activity row stayed on
"Creating issue" instead of transitioning to the normal MUL-XXX +
title rendering once the agent finished.
- Server: notifyQuickCreateCompleted now writes the resolved issue id
back to agent_task_queue.issue_id via a new LinkTaskToIssue query
(guarded by `issue_id IS NULL` so it only ever fills the unset
quick-create case). Best-effort: a write failure logs but doesn't
block the inbox notification.
- Frontend: defensive wording fallback — kind=quick_create rows in
terminal status (completed/failed/cancelled) now render as
"Quick create" instead of the active "Creating issue" label,
covering rows whose link write failed or whose agent never
produced an issue at all.
Members and agents previously appeared in fixed buckets (members first,
then agents) following raw cache order. Replace that with a single ranked
list driven by the user's most recent mentions on this device, with an
alphabetical fallback for never-mentioned targets. Recency is stored in
localStorage per workspace and lazy-pruned at 200 entries.
Recasts Quick/Advanced as Agent/Manual and lets users flip between modes
in-place from a footer switch button instead of a separate Advanced
shortcut. The two old modal types now route through one CreateIssueDialog
shell that owns the single <Dialog> and <DialogContent> — only the inner
panel body swaps on mode change, so the Portal/Backdrop/Popup stay
mounted and the switch is instant (no close→open animation flash).
Mode preference is persisted globally in localStorage via a small
useCreateModeStore, so the `c` shortcut always opens whichever mode the
user last used (or switched to). Carry payload (description / agent /
prompt) hands off through the shell's local state plus the existing
issue-draft store, so nothing the user typed is lost across switches.
Also drops the Shift+C → manual branch — `c` is now mode-agnostic and
the in-modal switch covers the same intent without users having to
remember a second shortcut.
Visible labels: "Quick create" → "Create with agent",
"New issue" → "Create manually".
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable
Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.
- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
(getColumnPinningStyle), adapted from Dice UI's registry. The shell
renders <table> directly (skipping shadcn's <Table> wrapper) so its own
outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
cell renderers. Row click navigates to detail via onRowClick (instead of
wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
templates and the sticky-first-col / subgrid mechanics that came with
them. agent-list-item.tsx is removed; runtime-list.tsx and
skills-page.tsx are trimmed to thin wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent): cap description at 255 chars (db + api + ui)
Symmetric enforcement across DB, server, and UI:
- Migration 060: pre-flight truncate of any oversize rows, then ADD
CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
(single source of truth shared by the create dialog + edit modal +
test suite) and a CharCounter component that warns at 90% and errors
past the cap.
- Description editor moves from a 288px popover to a roomy modal.
Editor body is mounted only while the dialog is open, so the local
draft state is locked in at mount time and never reset by an external
WS update — the React-recommended replacement for the
useEffect(reset, [value]) anti-pattern.
Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): data-table polish across runtime + skill lists
Builds on the DataTable migration in 2be0f287:
- Add ColumnMeta.grow flag — declared via TanStack module augmentation
in ui/lib/data-table.ts. Columns marked meta.grow skip their inline
width so fixed table-layout assigns them the leftover container space
(no spacer column). The Title-grows / others-fixed pattern from
Linear / GitHub PR rows.
- Authoritative table min-width = sum of column.size, applied to the
<table> itself (fixed-layout ignores cell-level min-width per spec,
so the floor has to live on the table).
- Header tightens to h-8 + uppercase + tracking-wider; pinned cells
switch to opaque bg + group-hover so they cover content scrolling
beneath them and follow row hover state.
- Toolbar slot removed from DataTable (callers wrap the toolbar
themselves now — keeps DataTable single-purpose).
Also: hover-card popup stops contextmenu / auxclick / dblclick from
bubbling out (in addition to click). Stops the popup from triggering
ancestor handlers (e.g. issue list rows) on right-click / middle-click
without breaking Base UI's outside-click dismiss, which listens to
pointerdown — pointerdown is deliberately NOT stopped.
Runtime + skill list pages updated to use the new sizing model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent): drop LastTaskState, introduce 3-state Workload
Continues the presence-model rework started in #1794 / #1798.
The previous LastTaskState union (running / completed / failed /
cancelled / idle) carried historical outcome at the list level — a
runtime-healthy agent whose last task failed showed a sticky red dot
indistinguishable from a daemon-dead agent.
New model: presence is two orthogonal "right-now" dimensions:
AgentAvailability — runtime reachability only (online / unstable /
offline). Drives the dot colour everywhere.
Workload — current load (working / queued / idle). Three
states, never historical. Failure / completion /
cancellation are surfaced via Recent Work + Inbox,
not list-level state.
`queued` (= nothing running, ≥1 queued) is an honest "stuck on offline
runtime" signal. To avoid amber flashes during the brief enqueue→claim
race on healthy runtimes, the queued chip composes with availability:
muted on online, warning amber otherwise.
Activity tab cleanup that follows from the new model:
- failureReasonLabel relocated from agents/presence.ts to
tabs/task-failure.ts (presence no longer owns historical state).
- Recent Work paginates (5 initial, +20 per "Show more"); chat-session
tasks are filtered out of every Agent-scoped surface to keep
"team work" separate from private chat.
- Agents page drops the lastTaskFilter chip group; users find broken
agents via Inbox / Recent Work, not a list-level filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(task): trigger summary snapshot + task:queued lifecycle event
Two task-lifecycle improvements that ship together because they share
the same enqueue/retry hot paths and changes interleave inside task.go:
1. trigger_summary snapshot (migration 061)
New nullable column on agent_task_queue. Comment-triggered tasks
snapshot the comment content; autopilot tasks snapshot the run title.
Truncated to 200 runes via strings.Builder so multibyte input counts
correctly without O(N²) concatenation. Snapshot survives source
edits/deletes — every task row self-describes across surfaces (issue
detail Execution log, agent activity tooltip, inbox) without joining
back to the originating row.
Retry rows inherit the parent's snapshot (CreateRetryTask SELECT) so
the description stays meaningful across attempts. The UI is
responsible for stacking "Retry #N" context on top.
2. task:queued WS event
New protocol event covering the ∅ → queued transition. Front-end
types/events.ts registers it; use-realtime-sync's task: prefix path
already invalidates task caches via onAny, so old clients without
this exact-match subscription still refresh correctly. Specific
subscribers (sticky banner) get sub-second updates instead of
waiting for daemon claim.
Retry path now broadcasts task:queued (not task:dispatch) — same
status transition shape as enqueue, so all "new task created" paths
agree on one event type.
Ordering: broadcastTaskEvent runs *before* notifyTaskAvailable so
the queued event is published into the WS bus before the daemon is
poked. Without this, a fast daemon could claim and emit task:dispatch
over the wire before the in-process queued broadcast fan-out reached
clients — race window is tiny but unsafe-by-construction.
Per-agent task list (agentTasksKeys.all) and per-issue task list
(["issues","tasks"]) added to the task: invalidation set so Activity
tab Recent Work and the Execution log section stay fresh.
Type contracts: AgentTask gains parent_task_id / attempt /
trigger_comment_id (already returned by the API, just missing from TS)
plus the new trigger_summary field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issue): ExecutionLogSection — unified active+past runs panel
Replaces two pieces:
- the click-to-expand timeline that lived inside AgentLiveCard
- the standalone TaskRunHistory below the main content
with a single right-panel section that lists every agent run for the
issue. Active runs sit at the top (always visible when present); past
runs collapse behind a "Show past runs (N)" toggle, sorted failed →
cancelled → completed within group.
Active rows show the trigger summary, status + relative time, and
Cancel / Transcript actions on hover (gradient backdrop fades the
status text rather than hard-clipping). Past rows show the same
shape minus Cancel.
Retry tasks prepend "Retry #N · " to the inherited summary so they're
distinguishable from their parent (which would otherwise share the
exact same trigger text).
Cache key registered as issueKeys.tasks(issueId); the global
useRealtimeSync task: prefix path already invalidates ["issues","tasks"]
on every task lifecycle event, so the section stays fresh without
local WS subscriptions.
AgentLiveCard slims down to a header-only "agent is working" sticky
banner — keeps the at-a-glance "is anyone working on this right now"
signal and the Stop / Transcript actions, drops the inline timeline
that ExecutionLogSection now owns. Subscribes to both task:queued and
task:dispatch so retries (which only emit queued) land in the banner
without waiting for daemon claim.
issue-detail mounts ExecutionLogSection in the right panel and removes
the now-defunct TaskRunHistory call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ActorAvatar prop was renamed in #1794 (split presence into
availability + last-task) — `disableHoverCard` is now `enableHoverCard`
with inverted semantics. The QuickCreate modal landed against the old
API and broke main's frontend typecheck. The two avatars in the modal
already want the default (no hover card), so just drop the prop
instead of opting in.
* feat(server): add quick-create issue async task path
Adds POST /api/issues/quick-create which validates the picked agent's
reachability up front (not archived, has runtime, runtime online) then
queues an issue-less agent task whose context JSONB carries the user's
natural-language prompt + requester + workspace. Daemon claim resolves
the workspace from the context, and the prompt builder switches to a
quick-create template instructing the agent to translate the prompt
into a single multica issue create call.
Task completion writes a success inbox item to the requester pointing at
the newly-created issue (located by querying the agent's most recent
issue in the workspace since task start, so we don't depend on agent
stdout shape). Failures write an action_required inbox item carrying the
original prompt + agent id so the frontend can offer "Edit as advanced
form" without losing input.
* feat(views): quick-create issue modal + inbox failure CTA
Adds a streamlined create-issue UI bound to the c shortcut: pick an
agent, type one line, submit. The modal closes immediately and the
agent translates the prompt into a multica issue create call in the
background. Shift+c keeps the legacy advanced form for users who want
every field. The "Advanced" button inside the new modal seeds the
shared issue-draft store with the prompt + picked agent so switching
mid-flow doesn't lose input.
Last-used agent persists per (user, workspace) via a workspace-aware
zustand store so frequent users skip the picker on every open.
Inbox renders quick_create_done items with a status pin to the new
issue and quick_create_failed items with an "Edit as advanced form"
CTA that re-seeds the legacy modal with the original prompt.
ApiError now carries the parsed JSON body so the modal can branch on
the structured agent_unavailable code without parsing the error
message.
* fix(quick-create): execenv injection, claim race, private-agent permission
Addresses GPT-Boy review on #1786:
1. execenv was rendering the assignment-task issue_context.md / runtime
workflow even for quick-create, telling the agent to call
`multica issue get/status/comment add` against an empty IssueID.
Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create
branch in renderIssueContext + the runtime_config workflow that
instructs the agent to run a single `multica issue create` and
exit, with explicit "do NOT call issue get/status/comment add"
guards.
2. ClaimAgentTask serialized only on issue_id / chat_session_id, so
concurrent quick-creates on the same agent (both NULL on those
columns) ran in parallel — making the success-inbox lookup race
over "most recent issue by this agent". Adds a third OR clause
that treats "all four FKs NULL" as a serialization key for the
same agent, so quick-create tasks on a given agent run one at a
time.
3. QuickCreateIssue handler bypassed the private-agent ownership rule
that validateAssigneePair enforces elsewhere — a user could POST a
private agent_id they didn't own and trigger it. Now routes the
picked agent through validateAssigneePair before the runtime
liveness check.
4. Clarifies the quick-create-store namespacing comment to match the
actual workspace-aware StateStorage convention used by the other
issue stores (per-user is browser-profile-local).
* fix(quick-create): branch Output section + deterministic origin lookup
Addresses GPT-Boy's second-pass review on #1786:
1. The runtime_config.go Output section forced "Final results MUST be
delivered via multica issue comment add" for every non-autopilot
task — quick-create still got this conflicting instruction even
though there's no issue to comment on. Switched the Output block
to a three-way switch so quick-create gets a tailored "stdout is
captured automatically; do NOT call comment add" branch matching
the autopilot variant.
2. Completion lookup was "most recent issue created by this agent
since task.started_at", which races against concurrent issue
creates by the same agent (assignment task running alongside
quick-create when max_concurrent_tasks > 1). Replaced with a
deterministic origin link:
- Migration 060 extends issue.origin_type CHECK to allow
'quick_create'.
- Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a
quick-create task.
- multica issue create CLI reads the env var and stamps the new
issue with origin_type=quick_create + origin_id=<task_id>.
- Server CreateIssue handler accepts (origin_type, origin_id)
from trusted callers (only "quick_create" is allowed; the pair
is rejected unless both fields are provided together).
- notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on
(workspace_id, "quick_create", task.ID) — no more time-window
racing against parallel agent activity.
The old GetRecentIssueByCreatorSince query is removed.
* fix(popover): stop click bubble + resilient presence loading
Two related bugs surfacing on production after #1794:
* Click-through: clicking a Detail link inside an agent hover card, or
a kebab item in agents/runtimes list rows, also fired the parent row
link's onClick. Base UI portals popovers in the DOM but React's
synthetic events still bubble through the React tree, so the
ancestor <a> wrapping the trigger still received the click. Fix at
the primitive level (HoverCardContent + DropdownMenuContent) so
every existing and future popover gets it for free — stopPropagation
on the popup's onClick, then forward consumer-supplied handlers.
* Presence loading forever: useAgentPresenceDetail returned "loading"
whenever any of its three queries had data === undefined. With prod
backend missing the new agent-task-snapshot endpoint (404), or with
an issue assignee referencing an archived agent (not in ListAgents),
the UI spun forever. Now: query errors degrade to empty arrays, and
a missing agent yields a synthesised offline+idle detail. The dot
still renders gray, hover card still shows "Agent unavailable" —
but no infinite skeleton.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(inbox): enable hover card on notification actor avatar
Originally excluded from the hover-card opt-in pass, but inbox
notifications are exactly the kind of "who sent me this?" surface
where seeing the actor profile on dwell is useful. Click-through to
the wrong target is no longer a concern — the popover stop-bubble
fix in this branch handles it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(autopilot): show agent presence dot on autopilots list rows
Autopilot detail / picker / dialog already render the dot — the list
was the lone holdout. With the autopilot-agent dependency this strong
("autopilot is dead if its agent is offline"), an at-a-glance dot is
the most useful signal in the row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type
Lays the API + type contract for the front-end agent presence cache:
- New `GET /api/active-tasks` returns active (queued/dispatched/running)
tasks plus failed tasks within the last 2 minutes for the current
workspace. The 2-minute window powers a UI-side auto-clearing "Failed"
agent state without back-end pollers.
- `agent_task_queue` has no workspace_id column, so the query JOINs agent;
`SELECT atq.*` keeps `failure_reason` (migration 055) on the wire.
- Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend
classifiers (agent_error / timeout / runtime_offline / runtime_recovery
/ manual) to copy without parsing free-text errors.
- New `api.getActiveTasksForWorkspace()` client method; workspace is
resolved server-side from the X-Workspace-Slug header (no path param,
matching /api/agents and /api/runtimes conventions).
Includes the joint engineering plan and designer brief that scope the
broader Agent / Runtime status redesign — Phase 0 is this contract plus
the front-end derivation layer landing in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge
Adds the front-end derivation layer that turns raw server data into the
user-facing 5-state agent / 4-state runtime enums. UI files are
deliberately untouched in this commit — derivation lives behind hooks
(useAgentPresence, useRuntimeHealth) that any component can call with
zero additional network traffic.
Architecture:
- Derivation is pure functions in packages/core/{agents,runtimes}; the
back-end stays free of UI translation. Agents algorithm: runtime
offline > recent failed (2-min window) > running > queued > available.
Runtimes algorithm: status + last_seen_at -> online / recently_lost /
offline / about_to_gc.
- A single workspace-wide active-tasks query backs all per-agent
presence reads, eliminating N+1 across hover cards, list rows, and
pickers. 30-second tick re-renders the hooks so the failed window
expires even when no underlying data changes.
- WS task lifecycle events (dispatch / completed / failed / cancelled)
invalidate active-tasks via the prefix dispatcher. completed/failed
were removed from specificEvents so they go through both the prefix
invalidate and the existing chat ws.on() handlers. Reconnect refetch
picks up active-tasks too.
- Desktop bridges window.daemonAPI.onStatusChange directly into the
runtimes cache via setQueryData, giving the local daemon sub-second
feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace
switches automatically rebind the subscription; daemon_id matching
covers the same-daemon-multiple-providers case.
24 derivation unit tests cover all branches plus null/empty/boundary
inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing
completed_at). Full core suite: 112 tests passing. Typecheck green
across all 8 workspace packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): redesign agent runtime status as two orthogonal dimensions
Splits the conflated 5-state agent presence into two independent axes:
- AgentAvailability (3-state): online / unstable / offline — drives the
dot indicator everywhere a dot appears. Pure runtime reachability;
never sticky-red because of a past task outcome.
- LastTaskState (5-state): running / completed / failed / cancelled /
idle — surfaced as text + icon on focused surfaces (hover card,
agent detail page, agents list, runtime detail). Never colours the dot.
Major changes:
* Domain layer: AgentPresence union → AgentAvailability + LastTaskState.
derive-presence split into deriveAgentAvailability + deriveLastTaskState
+ deriveAgentPresenceDetail orchestrator. Tests reorganised into three
groups (availability invariants, last-task invariants, composition).
* Visual config: presenceConfig (5 entries) → availabilityConfig (3) +
taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips.
* Workspace-level presence prefetch: new useWorkspacePresencePrefetch
hook + WorkspacePresencePrefetch mount component, wired into
DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards
render synchronously with no skeleton flash on first hover.
* ActorAvatar hover: flipped default — disableHoverCard removed,
enableHoverCard added (default false). Opt-in at ~14 decision-moment
surfaces; pickers / decoration sub-chips stay plain. Status dot
decoupled (showStatusDot prop) so picker rows can show presence
without nesting popovers.
* Hover cards: AgentProfileCard simplified — availability dot only,
Detail link top-right (logs live on the detail page). New
MemberProfileCard mirrors the structure: name + role + email +
top-2 owned agents (sorted by 30d run count) with click-through to
agent detail.
* Agents list: split Status into two columns — availability (3-color
dot + label) and Last run (task icon + label, optional running
counts). Two independent filter chip groups (Status + Last run);
combination acts as intersection ("online + failed" finds broken-
but-alive agents).
* Other UI surfaces (issue list/board/detail, comments, autopilots,
projects, runtimes, mention autocomplete, subscribers picker)
updated to the new dot semantics; status dot now strictly 3-color.
Server changes accompany the client redesign — workspace-wide
agent-task-snapshot endpoint, runtime usage queries, etc. — to feed
the derive layer with the data it needs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent-detail): drop last-task chip from detail header + inspector
The Recent work section on the agent detail page already shows the same
data (with task titles, timestamps, error context) — surfacing
"Completed" / "Failed" / etc. up in the header was redundant chrome.
Detail surfaces now show only the 3-state availability dot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tables): handle narrow viewports across agents / skills / runtimes
Three table layouts were squeezing content into adjacent cells at
intermediate widths. Each fix is small and targeted:
* runtime-list: the Runtime cell's base name had `shrink-0`, so it
refused to truncate when its grid column was narrowed under width
pressure — the name visually overflowed into the Health column
("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health
column was also a fixed 9.5rem reservation for the worst-case
"Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it
competes fairly with Runtime.
* skills-page: had a single grid template with no responsive
breakpoints — all 6 columns were rendered at any width and got
visually jammed below md. Added a <md template that drops Source +
Updated; the row markup hides those cells via `hidden md:block` /
`md:contents`.
* agent-list-item: the new Last run column was reserved at minmax(8rem,
max-content); on narrow md viewports the 8rem floor pushed the row
past available width. Changed to minmax(0,max-content) so the cell
shrinks under pressure (its content already truncates).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent-card): hover-only Detail + add Runtime row + breathing room
Three small polish tweaks to the agent hover card:
- Detail link gets `mr-1` + fades in only on card hover (group-hover).
It was visually flush against the popover edge and competing for
attention; now it stays out of the way during a quick glance and
surfaces only when the user is dwelling on the card.
- Runtime row is back, in the meta block (cloud/local icon + runtime
name). The earlier removal was over-aggressive — knowing where an
agent runs is part of "who is this agent". The wifi badge stays
dropped because the availability dot in the header already conveys
reachability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtime): wifi-style health icon (4-state) for runtime list + agent card
Replaces the 6px coloured dot with a wifi-shape icon that carries both
state (Wifi vs WifiOff) and severity (success/warning/muted/destructive).
Mapping:
- online → Wifi (success)
- recently_lost → WifiHigh (warning) — transient hiccup, fewer bars
- offline → WifiOff (muted) — long unreachable
- about_to_gc → WifiOff (destructive) — sweeper coming soon
Used in two places:
- Runtime list: replaces HealthDot in the dedicated leading-icon column.
Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized).
- Agent profile card RuntimeRow: derives runtime health from runtime +
clock (matching the 4-state semantics) and renders HealthIcon next
to the runtime name. Cloud runtimes always read as online. The
duplicate signal with the header availability dot is intentional —
it confirms WHICH runtime is the one currently in the dot's state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
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.
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>
* 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".
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.
* 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.
* 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.
- 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.
* 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.
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>
* 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.
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.
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>
* 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>
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>
* 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>
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>
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>
* 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>
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.
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>
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>
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>
* 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>
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>
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>