- 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.
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>
* 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>
* 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>
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>
#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>
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>
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>
* refactor(views): extract IssueChip shared primitive from mention card
IssueMention (in editor NodeView) and IssueMentionCard shared 95% of their
markup — StatusIcon + identifier + title inside a bordered chip. They drifted
into two parallel implementations so changes had to be made in two places.
Extract the presentational chip into IssueChip. The navigable variants
(IssueMentionCard, the editor NodeView) become thin shells that layer
routing + cmd/shift behaviour onto the shared chip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chat): add focus mode to share current page as context
Adds a Focus button next to the chat submit. When on, the chat auto-attaches
whatever the user is viewing (issue, project, or inbox-selected issue) as a
context prefix on outgoing messages, so the agent knows what "this" refers
to without the user pasting ids.
The attached object is derived from the route + react-query cache on every
render — no separate copy in state. Only the boolean focusMode is persisted
(global to the user, not per-workspace), matching the "my preference"
mental model.
The button has three visual states driven by two dimensions (focusMode +
whether the current route resolves to an anchorable object):
- off: ghost + muted, click turns on
- on + anchor: secondary (bright), click turns off
- on + none: disabled (nothing to attach here)
The derived anchor renders above the input as a chip — IssueChip for issues,
a new ProjectChip for projects — wrapped in AppLink so the visual target
matches the clickable target (mirrors IssueMentionCard's hover + navigation).
Prefix format reuses the editor's mention markdown:
Context: [MUL-1](mention://issue/<uuid>) — "Fix login bug"
Context: Project "Authentication"
so the agent sees an identical token whether the user @-mentioned inline or
focus-mode attached. Backend is untouched.
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(transcript): add multi-select tool filter to agent execution dialog
Adds a Filter dropdown to AgentTranscriptDialog that lets users
multi-select tool types (e.g. tool:Bash, tool:Edit) to narrow down
the event list, timeline bar, and copy output. Non-tool items (text,
thinking, error) are also filterable. Clear filters is placed at the
bottom of the dropdown with a separator.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transcript): address review comments on tool filter
- Replace index-based selection with seq-based (selectedSeq) to fix
highlight/scroll jumping when toggling filters
- Use full tool count for the "X tool calls" chip (task-level stat)
instead of filtered count
- Title-case filter labels: Thinking, Error (was lowercase)
- "Copy all" → "Copy filtered" when filter is active
- Replace raw <button> with DropdownMenuItem for Clear filters
so it participates in keyboard navigation
- Drop redundant idx from row key (seq is already unique/stable)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transcript): use proportional width in timeline bar
After removing segment grouping, the timeline bar lost proportional
widths. Restore proportional width per item (1/items.length * 100%)
so each event's width reflects its share of the timeline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transcript): show individual event dividers in timeline bar
When filtering by a single type (e.g. Agent), all events share the
same color and blend into one solid bar. Split each event into its own
clickable button so users can see and click individual events, while
keeping proportional widths based on item count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transcript): simplify timeline bar to segment-level buttons
Remove per-item nested buttons in timeline segments; each segment is now
a single clickable area. Reduces DOM nodes and aligns with the original
design where segments are coarse color blocks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(transcript): reuse getEventLabel for filter option display
Replace the manual displayMap with getEventLabel() so filter option
labels stay in sync with row labels automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transcript): address round-2 review comments
- Remove dead `onClick` prop from TranscriptEventRow (caused
TS6133 under noUnusedParameters; row never wired a click handler)
- Align `itemFilterKey` guard with `filterOptions` derivation
(tool_use/tool_result type check)
- Fix TimelineBar `isSelected` from seq range to actual membership
via `.some()` — avoids false highlight when a filtered-out seq
falls within a segment's range
Note: `DropdownMenuItem` uses `onClick` not `onSelect` because this
codebase uses Base UI, not Radix. Base UI's Item.Props has no
`onSelect`; the inbox/members-tab code uses `onClick` as the pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Ark <lifangzhou@shizhuang-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(issues): paginate every status column, not just done
Previously the workspace issues list fetched all non-done/cancelled
issues in a single unbounded `open_only=true` request and only
paginated the done column. In workspaces with many open issues this
ballooned the initial payload and skipped pagination entirely.
Restructure the issue list cache into per-status buckets
(`{ byStatus: { [status]: { issues, total } } }`) fetched in parallel,
generalize `useLoadMoreDoneIssues` into `useLoadMoreByStatus(status,
myIssuesOpts?)`, and render an infinite-scroll sentinel inside every
accordion group and kanban column. Sort and filter stay client-side,
matching the done column's existing behavior.
Backend `ListIssues` already supports per-status pagination, so no
API changes are required.
* fix(issues): handle project / hidden-column / lookup regressions from paginated list cache
After bucketing the issue list cache by status, three consumers that
treated `issueListOptions()` as a complete local index broke:
- `project-detail.tsx` filtered the workspace list by `project_id`
client-side, so projects whose issues sat past the first 50-per-status
page rendered empty. Switch to `myIssueListOptions(wsId,
'project:<id>', { project_id })` so the server returns only this
project's issues; add `project_id` to `ListIssuesParams` /
`MyIssuesFilter` / api client.
- `board-view.tsx` HiddenColumnsPanel read counts from the in-memory
`issues` array — a paginated fragment. Pass `myIssuesOpts` through to
a per-row subcomponent that reads the real per-status total from the
cache.
- `tasks-tab.tsx` and `search-command.tsx` used the list as a global
lookup for task titles / Recent items / current-issue chrome. Switch
both to per-id `issueDetailOptions` via `useQueries` so they're
independent of which page the issue lands on.
Drop the now-redundant `doneTotal` override prop on BoardView/ListView
and the `allIssues` prop on BoardView (only HiddenColumnsPanel consumed
it).
Tests updated: tasks-tab now mocks `api.getIssue`; search-command mocks
`issueDetailOptions` + `useQueries`; project-issue-metrics drops the
`doneColumnCount` assertion.
AgentLiveCard kept its taskStates map across issueId prop changes, and
its merge logic only added newly-fetched tasks without removing stale
ones. Navigating from Issue A (with a running agent) to Issue B via
cmd+k left A's sticky agent status card pinned on B's page.
Key AgentLiveCard and TaskRunHistory by issue id so React remounts
them when the issue changes, guaranteeing fresh state per issue.
Closes MUL-1147
Adds a "Create sub-issue from selection" button to the editor bubble
menu. When an issue context is present (description editor, comment
input, reply input, comment edit), selecting text and clicking the
button creates a new issue parented under the current issue and
replaces the selection with a mention link to the new issue.
Previously the issue detail top bar only showed 'workspace name > identifier'.
Add the issue title next to the identifier so users can see what issue they're
viewing without scrolling.
Adds two new toggleable card properties that surface issue context at a glance:
- Project: shows the parent project icon + title when the issue belongs to one.
- Sub-issue progress: gates the existing progress ring behind a card property
so users can hide it when not useful.
Both default to on; toggled via the existing "Display" popover.
Images pasted into comments could produce duplicate attachment records
(macOS/Chrome clipboard provides duplicate File entries), causing
AttachmentList to show a spurious file card below the inline image.
Three-layer fix:
- Dedup clipboard files by name+size+type in paste/drop handlers
- Track upload URL→ID mapping instead of accumulating IDs blindly;
only send IDs for uploads still present in content on submit
- AttachmentList filters duplicate attachments (same file identity)
where a sibling is already referenced inline — handles old data
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(issues): persist comment collapse state across page reloads
Store collapsed comment IDs in a workspace-scoped Zustand store backed
by localStorage, replacing the transient useState(true) default.
Comments now remember their collapsed/expanded state per issue.
* test(issues): add useCommentCollapseStore mock to issue-detail tests
The existing vi.mock for @multica/core/issues/stores didn't include the
newly exported useCommentCollapseStore, causing CommentCard to throw at
render time.
* feat(agent): add Pi agent runtime support
Add Pi as a new agent runtime provider, following the established adapter
pattern. Pi CLI outputs JSONL events which are parsed for messages, tool
calls, and usage tracking.
Backend:
- New piBackend implementing the Backend interface (pi.go)
- Pi CLI discovery via MULTICA_PI_PATH env var or PATH lookup
- JSONL event stream parsing (agent_start, message_update, thinking_update,
tool_execution_start/end, agent_end)
- Usage scanner for ~/.pi/sessions/*.jsonl files
- Runtime config injection via AGENTS.md
- Skill injection to .pi/agent/skills/
Frontend:
- Pi provider logo (teal π icon)
- Pi label in transcript dialog
Docs:
- Updated all provider lists in README, CLI_INSTALL, and docs
* fix(agent): filter Pi usage scanner to agent_end events only
Address review feedback: restrict usage parsing to agent_end events
which contain cumulative totals, preventing potential inaccuracy if
Pi adds usage fields to other event types in the future.
* fix(agent): align Pi runtime with real CLI flags, event schema, and custom_args
- Flags: Pi's CLI uses `--mode json` (not `--output-format jsonl`), has no
`--yolo` (explicit `--tools` allowlist instead), takes the prompt as a
positional argument (not `-p <prompt>`), splits model as
`--provider <name> --model <id>`, and treats `--session` as a file path
that must exist before spawn.
- Event parsing: rewrite the stream event struct to match Pi's actual
JSON event schema (`message_update.assistantMessageEvent.delta`,
`turn_end.message.usage.{input,output,cacheRead,cacheWrite}`, etc.).
- Sessions: generate/persist session files under ~/.multica/pi-sessions/
and use the file path as the opaque SessionID returned to the daemon.
- Usage scanner: read assistant `message` events from the same session
files (Pi's session-file schema, distinct from the stdout stream).
- Custom args: consume `ExecOptions.CustomArgs` via `filterCustomArgs`
with a Pi-specific blocked set (`-p`, `--print`, `--mode`, `--session`)
so Pi matches the pattern shared by every other agent backend.
* Reapply "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit 9b94914bc8.
* compat: legacy URL redirect + localStorage double-write for safe rollback
The first attempt at this refactor (#1131) was reverted because existing
users on old URLs (/issues, /projects, etc.) hit 404 immediately after
deploy, and rolling back left them with empty dashboards — the legacy
code reads localStorage["multica_workspace_id"] to attach a workspace
to API requests, but the new code had stopped writing that key.
Two compat layers added on top of the restored refactor:
1. proxy.ts now intercepts legacy route prefixes (/issues/*, /projects/*,
/agents/*, /inbox/*, /my-issues/*, /autopilots/*, /runtimes/*,
/skills/*, /settings/*). Logged-in users with a last_workspace_slug
cookie are 302'd to /{slug}/{rest}, preserving their deep link. Users
without the cookie bounce through / where the landing page picks a
workspace client-side. Unauthenticated users go to /login.
2. Both layouts now double-write the workspace id to the legacy
localStorage key on every workspace entry. New code ignores this key
— it exists solely so that if this PR ever gets reverted again, the
legacy build reading the key would still find the correct workspace
and avoid the empty-dashboard symptom users saw during the rollback.
Net effect: any direction of deploy ↔ rollback is now cache-compatible,
and any direction of old bookmark → new route resolves without 404.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(platform): defer rehydrateAllWorkspaceStores to a microtask
Same React 19 render-phase restriction that forced setCurrentWorkspace
to defer its subscriber notifications. rehydrateAllWorkspaceStores
synchronously calls each persist store's rehydrate, which setState()s
the store, which schedules updates on any subscribed component. When
the workspace layout's render-phase ref guard invoked this, React
complained that SearchCommand (a store subscriber) couldn't be
re-rendered while WorkspaceLayout was still rendering.
Fix: queueMicrotask the rehydrate loop and add a pending-flag guard so
rapid workspace switches coalesce into one rehydrate on the final slug.
Persist stores tolerate one microtask of staleness — they hold UI
preferences, not correctness-critical state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: workspace URL refactor + slug-first API identity
Make the URL the single source of truth for workspace identity.
All workspace-scoped URLs now carry the workspace slug as the first
path segment (/{slug}/issues, /{slug}/projects, etc.), matching the
industry standard (Linear, Notion, Vercel, GitHub).
## Key architectural changes
**URL-driven workspace identity:**
- Web routes moved under app/[workspaceSlug]/(dashboard)/
- Desktop routes nested under /:workspaceSlug
- paths.ts builder centralises all URL construction
- reserved-slugs validation (backend + frontend + DB migration audit)
**Slug-first API contract:**
- Frontend sends X-Workspace-Slug header (from URL) instead of X-Workspace-ID (UUID)
- Backend middleware resolves slug → UUID via GetWorkspaceBySlug, falls back to
X-Workspace-ID for CLI/daemon backwards compatibility
- WebSocket auth accepts ?workspace_slug query param with SlugResolver callback
**State cleanup:**
- Deleted: useWorkspaceStore (Zustand mirror), switchWorkspace/hydrateWorkspace/
clearWorkspace, localStorage["multica_workspace_id"], api._workspaceId
- useCurrentWorkspace() derives from URL slug + React Query workspace list
- useWorkspaceId() is now a bridge hook (no Context, derives from useCurrentWorkspace)
- WorkspaceIdProvider removed from DashboardGuard
- Paired module vars (slug + UUID) in workspace-storage.ts for non-React consumers
**Layout simplified:**
- Render-phase ref guard sets workspace context synchronously (no async gate)
- DashboardGuard handles auth redirect, loading state, and workspace resolution
- Subscriber notifications deferred via queueMicrotask (React 19 compat)
- persist namespace uses slug (immutable) instead of UUID
## Issues resolved
MUL-43 (share links), MUL-509 (mobile workspace switch), MUL-723 (workspace in URL),
MUL-727 (create workspace flash), MUL-728 (delete workspace no-navigate),
MUL-820 (sidebar Join not switching)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve code review C3/C4/C5/C6 — desktop deadlock + hardcoded paths
C3: Desktop OnboardingGate was calling useCurrentWorkspace() outside
WorkspaceSlugProvider → always null → permanent onboarding deadlock.
Rewrite to use useQuery(workspaceListOptions()) which reads React Query
cache directly without slug context. Remove DashboardGuard from
DesktopShell (auth gating handled by AppContent, workspace routing by
WorkspaceRouteLayout per-tab).
C4: Landing page "Dashboard" links hardcoded /issues (no longer valid).
Changed to / — proxy handles redirect to /{lastSlug}/issues.
C5: autopilots-page.tsx had one hardcoded /autopilots/${id} link.
Changed to wsPaths.autopilotDetail(id).
C6: inbox-page.tsx hardcoded /inbox paths. Changed to wsPaths.inbox().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): wrap shell in WorkspaceSlugProvider from module var
AppSidebar calls useWorkspacePaths() → useRequiredWorkspaceSlug() which
throws outside WorkspaceSlugProvider. In the desktop shell, the sidebar
renders at the shell level (outside any tab's WorkspaceRouteLayout).
Fix: DesktopShell reads the current slug via useSyncExternalStore on
the workspace-storage singleton. When slug is available, wraps the
entire shell in WorkspaceSlugProvider. When null (first mount before
any tab's WorkspaceRouteLayout sets it), shows a loading spinner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): migrate old tab paths + fix shell slug deadlock
Tab store rehydration: old-format paths like "/issues/abc" (missing
workspace slug prefix) are reset to "/" so IndexRedirect picks the
correct workspace. Detection: if the first segment is a known route
name (issues, projects, etc.) rather than a workspace slug, it's an
old-format path.
Desktop shell: TabContent must always render (not gated behind slug
check) so WorkspaceRouteLayout can mount and call setCurrentWorkspace.
Only sidebar and shell-level UI (chat, modals, search) gate on slug
being present.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two editor bugs fixed:
1. Descriptions saved unnecessarily on every document change (no dirty
check). Added onCreate baseline capture + string comparison in the
debounced onUpdate handler so mutations only fire when content
actually changes.
2. Clearing a description didn't persist — empty string was converted
to undefined via `md || undefined`, causing the field to be omitted
from the API request. Changed to `md` so empty strings reach the
backend and clear the description via COALESCE.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(server): trigger agent when issue moves out of backlog
When a member moves an agent-assigned issue from "backlog" to an active
status (e.g. "todo", "in_progress"), enqueue an agent task so the agent
starts working. This lets backlog act as a parking lot where issues can
be assigned to agents without immediately triggering execution.
Applies to both single and batch issue updates.
* fix(server): treat backlog as parking lot — no trigger on create/assign
Address review feedback: creating or assigning an agent to a backlog
issue no longer triggers immediate execution. Only moving out of backlog
to an active status triggers the agent, producing exactly one task.
- shouldEnqueueAgentTask now gates on backlog status
- backlog→active trigger uses isAgentAssigneeReady directly
- Added TestBacklogNoTriggerOnCreate test
- Updated TestBacklogToTodoTriggersAgent to assert exactly 1 task
across the full create→move path (no manual cleanup)
* feat(ui): show toast hint when assigning agent to backlog issue
Users may not know that backlog issues won't trigger agent execution
until moved to an active status. Show an actionable toast with a
"Move to Todo" button when:
- Assigning an agent to a backlog issue in the detail page
- Creating a backlog issue with an agent assignee
* feat(ui): add "Don't show again" option to backlog agent toast
Users who understand the backlog parking lot behavior can dismiss the
hint permanently. Uses localStorage to persist the preference.
* feat(ui): replace backlog agent toast with AlertDialog
Use a modal dialog instead of a toast notification so users must
explicitly acknowledge the hint. The dialog offers three options:
- "Move to Todo" — changes status and triggers the agent
- "Keep in Backlog" — dismisses without action
- "Don't show again" — persists dismissal in localStorage
* fix(ui): improve backlog agent dialog
* fix(ui): close create dialog behind hint, use checkbox for don't-show-again
1. Create Issue dialog now closes when the backlog agent hint appears,
so only the hint dialog is visible (not stacked behind).
2. "Don't show again" is now a checkbox instead of a separate button.
When checked, clicking either "Keep in Backlog" or "Move to Todo"
persists the preference.
* fix(ui): smooth backlog agent hint dialog
* fix(test): add useUpdateIssue mock to create-issue test
The test mock for @multica/core/issues/mutations was missing the
useUpdateIssue export that create-issue.tsx now imports, causing
CI failure.
Recent issues store was duplicating server data (title, status, identifier)
in Zustand, violating the single-source-of-truth architecture. Now the store
only tracks visit records (id + visitedAt), and the search command joins
fresh data from the TanStack Query issue list cache at render time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Issue mentions in comments showed only the identifier (no status icon
or title) after page refresh when the referenced issue wasn't in the
issueListOptions cache (e.g. done issues beyond the first 50).
Fall back to issueDetailOptions to fetch the individual issue when it's
not found in the list. The detail query is only enabled when the issue
is missing from the list, so it adds no overhead for the common case.
Replace Tiptap's BubbleMenu plugin with @floating-ui/react-dom for
all floating editor UI (formatting toolbar, link preview cards).
Architecture:
- useFloating({ strategy:"fixed" }) + createPortal(body) escapes
all overflow:hidden ancestors (Card component, scroll containers)
- autoUpdate + contextElement monitors all scroll ancestors for
repositioning; manual update() on transaction for virtual ref changes
- open prop resets isPositioned on visibility change (no stale-position
flash at 0,0)
- display:none for hiding (not return null which causes blur/focus
cycle, not visibility:hidden which leaves transition artifacts)
- No blur listener — portal DOM updates cause false editor blurs;
outside-click + scroll + resize + Escape handle all close cases
Bug fixes:
- BubbleMenu: remove all custom visibility hacks, let selection state
drive show/hide
- Link preview: new shared card (Copy + Open) for editable editor and
readonly markdown, portaled to body with fixed positioning
- TitleEditor: use JSON content format (not HTML interpolation that
loses < > characters)
- Blob URLs: strip from getMarkdown output during upload
- Markdown paste: check clipboard.files first to avoid intercepting
file paste events
- FileCard: escape HTML attributes in preprocessing
- Link extension: enable linkOnPaste, set defaultProtocol to https,
switch URL normalization to protocol blocklist (only block
javascript:/data:/vbscript:)
Dependencies: add @floating-ui/react-dom, remove @tiptap/extension-bubble-menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sub-issue progress indicator (e.g. "0/2") was undercounting because
it was computed from the client-side issue list, which only loads the
first 50 done issues. Sub-issues marked as done beyond that page were
excluded from both the total and done counts.
Added a dedicated backend endpoint (GET /api/issues/child-progress) that
aggregates child issue counts directly from the database, ensuring
accurate totals regardless of client-side pagination or filtering.
Fixes MUL-702
* feat(views): add keyboard navigation and auto-select to PropertyPicker
Add arrow key (up/down) navigation and Enter key selection to the
searchable PropertyPicker dropdown. When the search narrows results to
a single match, pressing Enter auto-selects it without needing to
arrow-navigate first. Fixes GitHub issue #793.
* fix(views): hide Unassigned option when search filter is active
When the user types a search query in the assignee picker, the
Unassigned option is no longer pinned at the top — it only shows
when there is no active filter.
* feat(views): auto-highlight first result when searching in picker
broadcastTaskDispatch was the only task event broadcast missing issue_id
in its WebSocket payload. The frontend task:dispatch handler had no way
to filter by issue, causing AgentLiveCard to briefly show activity for
the wrong issue when multiple tabs are open.
Closes https://github.com/multica-ai/multica/issues/791
Replace LIMIT $2 with AND date >= $2 in ListRuntimeUsage query. When a
runtime uses multiple models each day has multiple rows, so a row LIMIT
silently returns fewer days than requested.
Also fixes displayName warnings in issue-detail test mocks and adds
missing setOpen to useCallback deps in search-command.
Co-authored-by: jayavibhavnk <jaya11vibhav@gmail.com>
Closes#731
When navigating to an issue where an agent is already working, the
"Agent is working" card was delayed because it waited for both
getActiveTasksForIssue() AND listTaskMessages() to complete before
rendering. Now the card renders immediately after active tasks are
fetched, and messages load progressively in the background. Also
properly merges HTTP-loaded messages with any WebSocket-delivered
messages to avoid race conditions.
commentToTimelineEntry() was dropping the attachments field, and
comment-card never rendered entry.attachments. Attachments uploaded
through the CLI (not embedded in markdown) were invisible in the UI.
- Add attachments to commentToTimelineEntry() conversion
- Add AttachmentList component that renders standalone attachments
(skipping those already referenced in the markdown content)
- Render AttachmentList in both CommentRow and CommentCard