30 Commits

Author SHA1 Message Date
Naiyuan Qing
a02b3dfb4a feat(issues): move agent live signal into the issue-detail header (#3879)
* feat(issues): move agent live signal into the issue-detail header

Replace the in-body sticky "agent is working" card (AgentLiveCard) with a
compact chip in the issue-detail header, so the live signal sits in one
fixed place and never competes with sticky banners in the content column.

- New IssueAgentHeaderChip: avatar(s) + live-ticking blue elapsed time;
  click opens a popover listing every active task.
- Popover reuses ExecutionLogSection's ActiveTaskRow (now exported) so the
  popover and the right panel are literally the same row — no duplication.
- PopoverContent gains an optional keepMounted so the row's confirm dialog
  survives the popover closing on Stop.
- Running rows in ExecutionLogSection drop the blue spinner for a
  live-ticking blue elapsed timer (panel + popover share this).
- Source the chip from the workspace agent-task snapshot filtered by issue
  (same source as board/list indicators, zero extra network); delete the
  old AgentLiveCard + its test and its heavy per-issue WS machinery.

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

* feat(issues): live event count on the agent chip + execution-log rows

Show a live "N events (elapsed)" on running agents, consistent across the
header chip, its popover rows, and the right-panel execution log.

- Read the shared per-task message cache (taskMessagesOptions, kept live by
  useRealtimeSync's global task:message handler) instead of a bespoke
  subscription — one source of truth, deduped across chip / popover / panel /
  transcript, no extra WS wiring.
- Extract <RunningStat> (event count in info-blue + elapsed in muted parens)
  so all surfaces render the running stat identically.
- ExecutionLogSection running rows now show the same "N events (elapsed)";
  the transcript opened from them streams live from the shared cache.
- Chip: single running shows events (elapsed); multiple shows "N working".
- i18n: add agent_live.event_count (4 locales).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:51:20 +08:00
Naiyuan Qing
bd1fb10afa chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute
- Replace useContext() with React 19 use() across 16 context consumers
- Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files
  (mobile excluded — Hermes lacks toSorted support)
- Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check
- Fix nested component definition: useMemo wrapping HeaderRight → useCallback
- Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad

React Doctor score: 23 → 30. No behavioral changes, no business logic modified.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:57:07 +08:00
李冠辰
963ed5cd0e feat(comments): allow selecting multiple attachments 2026-05-27 10:13:27 +08:00
Qi Yijiazhen
d9ae891064 fix(avatar): stop bg-muted bleeding through transparent images (#2670)
ActorAvatar applies bg-muted on its container regardless of whether
an image is loaded, so transparent regions of PNG/SVG avatars reveal
the grey placeholder. agent-detail-inspector also wraps ActorAvatar
in an outer bg-muted div, layering a second grey square.

Make bg-muted conditional on the fallback state in ActorAvatar, and
drop the redundant bg-muted from avatar-picker's image-loaded branch
and the two inspector wrappers. Empty-state placeholders unchanged.
2026-05-18 18:23:46 +08:00
YYClaw
8cc48b1176 fix(ui): vertically center SelectItem content (#2782) 2026-05-18 15:28:00 +08:00
Jiayuan Zhang
3698fd85d5 feat(views): show Total in daily token/cost chart tooltips (MUL-2282) (#2704)
* feat(views): show Total in daily token/cost chart tooltips (MUL-2282)

Add a Total row at the bottom of the daily-tokens-chart and daily-cost-chart
tooltips so users can see the precise stack sum on hover, in addition to the
per-stack breakdown.

Implemented by extending shared ChartTooltipContent with an optional `footer`
prop (ReactNode | (payload) => ReactNode) that renders below the items with a
top divider; backwards-compatible (no behavior change when footer is omitted).

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): i18n Total label in chart tooltips (MUL-2282)

Lint rule i18next/no-literal-string flagged the hardcoded "Total" string
in daily-cost-chart and daily-tokens-chart tooltips. Move it to
runtimes.charts.tooltip_total and read via useT.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 15:50:20 +02:00
LinYushen
29082f7cfe feat: implement Squad feature MVP (#2505)
* feat: implement Squad feature MVP

- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
  - Assign Squad immediately triggers leader
  - Every external comment on squad-assigned issue triggers leader
  - Anti-loop: squad members' comments don't trigger leader
  - Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted

Co-authored-by: multica-agent <github@multica.ai>

* fix: address PR review blocking issues

1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types

Co-authored-by: multica-agent <github@multica.ai>

* feat: soft-delete squads via archive instead of hard delete

- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields

Co-authored-by: multica-agent <github@multica.ai>

* feat: re-add Squads frontend entry (sidebar nav + pages)

Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'

Co-authored-by: multica-agent <github@multica.ai>

* fix: fix SquadsPage rendering - use PageHeader children pattern

PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.

Co-authored-by: multica-agent <github@multica.ai>

* fix(squads): add API client methods and package export for squads pages

* feat: complete Squad frontend - create dialog, member management, API methods

- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
  addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
  - Member list showing resolved names
  - Add/remove member UI
  - Archive squad button
  - Back navigation to squads list

Co-authored-by: multica-agent <github@multica.ai>

* feat: improve Squad UI - match create agent dialog style

- CreateSquadModal: proper Dialog with Header/Description/Footer,
  agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
  Crown badge for leader, textarea for member description,
  improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
  (describes the member's responsibilities in the squad)

Co-authored-by: multica-agent <github@multica.ai>

* feat(squad): add avatar, instructions; drop unique-name constraint

- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
  name are legitimate across teams; uniqueness was an accidental
  product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
  UpdateSquad

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(squad): assignable + mention target; trigger leader on assign

- assignee picker and @mention suggestion list squads alongside
  agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
  a task for the squad's current leader (mirrors agent-assignee
  parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
  pickers
- locales updated for new picker copy

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(squad): agent-style detail page with members + instructions tabs

- restructure squad detail page to mirror the agent detail page:
  320px inspector (creator, leader, created/updated) + tabbed
  pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
  swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(squad): inject leader briefing on claim (protocol + roster + instructions)

When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:

1. Squad Operating Protocol — hard-coded rules: leader is a
   coordinator, dispatch via @mention, stop after dispatching,
   resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
   member with a literal mention markdown string ([@Name](mention://
   agent|member/<UUID>)) the leader can paste verbatim. Round-trips
   through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
   omitted entirely when empty so we do not leave a dangling heading.

Non-leader members claiming the same issue receive no briefing.

Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(squad): tell leader not to restate issue context in dispatch comment

After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:

- assignees on Multica already have the full issue (title,
  description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
  (who is picked, why, extra constraints), aim for two or three
  sentences;
- restating context is now an explicit hard rule violation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(squad): unify leader evaluation into activity_log, add CLI command

- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code

* feat(cli): add squad list, get, member list commands

* fix(squad): address review findings (P1+P2)

P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)

P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees

* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive

- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards

* fix(squad): realtime sync, rerun support, leader validation

- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-13 18:46:20 +08:00
Naiyuan Qing
19c40c5d68 fix(ui): translate hardcoded English strings in shared ui package (#2526)
The four user-visible strings exposed by packages/ui rendered untranslated
on every page that used them:

- file-upload-button.tsx — "Attach file" aria-label/title
- sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title
- pagination.tsx — "Go to previous/next page" aria-labels
- CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip

Root cause: the package had no i18n hookup at all because the package
boundary rule forbids importing @multica/core. Replicating the pattern
five times would have been the same hack five times. Hooking up
react-i18next directly is the structurally clean fix — i18next is a
generic library, not business logic, and the upstream I18nextProvider
already exposes the instance via context.

To let packages/ui typecheck the selector form standalone (i.e. without
the views resource-types augmentation in scope), the augmentation is
split: views declares everything except the `ui` namespace on a new
global `I18nResources` interface, and packages/ui contributes the `ui`
slice via declaration merging in packages/ui/types/i18next.ts. Views'
resources-types side-effect-imports that file so both packages see the
merged shape during downstream typechecks.

Scope intentionally excludes:
- packages/ui/components/common/error-boundary.tsx — keeping its fallback
  in English so a render-time crash never depends on i18n being healthy.
- apps/desktop/src/renderer/src/components/update-notification.tsx —
  ships with the next desktop release, not via this PR.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 18:25:40 +08:00
Bohan Jiang
e3e61c161c fix(inbox): show Multica logo for system-actor notifications (#2479)
Notifications from system actors (e.g. GitHub PR closed) were rendering
with an "S" initials fallback. The avatar now shows the Multica icon
when actor_type === "system", matching the platform's brand.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-12 18:06:42 +08:00
Naiyuan Qing
34a7ba9865 fix(chat): unify chat and comment send shortcut to Mod+Enter (#2398)
Chat input had `submitOnEnter` enabled while the comment editor used
`Mod+Enter`. Two consequences:

- Inconsistent muscle memory between the two inputs.
- In chat, bare Enter sending stole the only key that continues a
  TipTap bullet/ordered list. Shift+Enter falls through to HardBreak
  (a <br> inside the same list item), so bullet lists were stuck at
  one item.

Drop `submitOnEnter` from the chat input so it follows the editor
default. Mod+Enter (⌘↵ / Ctrl+Enter) sends in both places; bare Enter
now continues lists and inserts paragraphs as users expect.

Surface the shortcut on the SubmitButton via a new optional `tooltip`
prop, and route the comment input through SubmitButton instead of an
ad-hoc Button — same affordance, deduped.

Add unit coverage for the submit-shortcut extension that pins
Mod-Enter, the submitOnEnter=false case, IME, and code-block guards.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 14:39:37 +08:00
Jiayuan Zhang
bb3d2b70ea fix(ui): let DropdownMenu popup size to content (#2306)
DropdownMenuContent had `w-(--anchor-width)` which locks the popup
width to the trigger. With icon-sm kebab triggers (~32px) the popup
was clamped by `min-w-32` to 128px, and longer items like
"Unresolve thread" / "标记为已解决" wrapped onto two lines.

Anchor-width matching is the right behavior for Select / Combobox
(both keep that class), but a generic kebab menu should size to its
own content. Drop the `w-(--anchor-width)` and keep `min-w-32` as the
floor.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-09 07:07:58 +02:00
Naiyuan Qing
48e3131bf9 feat: harden desktop frontend against API response drift (MUL-1828) (#2208)
* docs(claude): add API Response Compatibility section

Narrows the existing "no backwards compat" rule to internal code only,
and adds a new section that codifies the defensive boundary at API
edges: parse-don't-cast, never pin UI to a single field, enum drift
must downgrade not crash.

Driven by #2143/#2147/#2192 — all three were the desktop client white-
screening on backend response shape changes the client wasn't built
against.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(core): add zod-based API response validation layer

Introduces a defensive boundary so a malformed backend response
degrades into a safe fallback (empty page, [], etc.) instead of
throwing inside React render.

- Adds zod to the pnpm catalog and as a @multica/core dependency.
- New parseWithFallback helper in core/api/schema.ts that runs
  safeParse, logs a warn with the endpoint + zod issues on failure,
  and returns the caller-supplied fallback. Never throws.
- Schemas in core/api/schemas.ts are deliberately lenient (string
  enums kept as z.string() so unknown values still parse, optional
  fields default, nested records use .loose() for unknown keys).
- Wires setSchemaLogger from CoreProvider so warnings flow through
  the same logger as the rest of the API client.

This is the primitive — see the next commit for the call-site wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(api): guard top 5 high-risk endpoints with parseWithFallback

Wraps the response of the five endpoints whose UIs white-screened in
past incidents (#2143/#2147/#2192) so a contract drift returns a safe
fallback instead of crashing the consumer:

- listIssues          → ListIssuesResponseSchema, fallback { issues: [], total: 0 }
- listTimeline        → TimelinePageSchema,        fallback empty page
- listComments        → CommentsListSchema,        fallback []
- listIssueSubscribers → SubscribersListSchema,    fallback []
- listChildIssues     → ChildIssuesResponseSchema, fallback { issues: [] }

getIssue is intentionally NOT wrapped: there is no sensible "empty
issue" — the entire detail page depends on real fields. The page-level
ErrorBoundary (separate commit) catches that case.

Adds schema.test.ts with 9 cases covering the five failure modes
listed in MUL-1828: missing fields, wrong types, enum drift, null
body, and null arrays.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(ui): add ErrorBoundary and wrap high-risk pages

Section-level error boundary (no third-party dep — class component +
default fallback in @multica/ui). Supports a fallback render prop and
resetKeys for auto-recovery on resource navigation.

Wraps the surfaces that white-screened in past incidents:

- IssueDetail (web + desktop + inbox split-pane) — keyed on issueId
  so navigating to a different issue clears the boundary automatically.
- IssuesPage (web + desktop).

Boundaries are placed at consumer call sites rather than inside
IssueDetail itself so we don't have to refactor the 1100-line
component, and so a crash inside one inbox split-pane doesn't take
down the inbox list next to it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(core): make all API schemas .loose() to preserve unknown fields

zod 4 z.object() defaults to STRIP, which silently drops fields the
schema didn't list. That makes the schema layer a sync point: a future
PR adding a TS field but forgetting the schema would have the field
disappear at runtime while TS still claims it exists — the exact bug-
class this PR is meant to prevent, just inverted.

Apply .loose() to every object schema (TimelineEntry, TimelinePage,
Comment, Issue, ListIssuesResponse, Subscriber, ChildIssuesResponse)
so unknown server-side fields pass through unchanged. Add a regression
test that feeds a payload with extra fields at both entry and page
level, and a direct unit test for parseWithFallback decoupled from any
endpoint. Update the listIssues fallback test to use a wrong-type
payload — under .loose() the previous "{ unexpected: true }" payload
parses successfully (every declared field has a default) instead of
triggering the fallback path it was meant to exercise.

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

* docs(claude): strip field-specific examples from API Compatibility section

The original wording embedded current schema field names (entries,
has_more_before, has_more_after, cursor, status, type) directly in the
rules. CLAUDE.md should state the rule, not the implementation — once a
field is renamed the doc drifts out of sync with the code, and the
specific names don't add anything the abstract rule doesn't.

Keep the rule, drop the field-level archaeology.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-07 15:09:55 +08:00
Naiyuan Qing
949dffdf7e feat: permission-aware UI across agent/comment/runtime/skill surfaces (#1915)
* feat(permissions): add core permission module and shared UI primitives

Foundation for permission-aware UI: pure rules that mirror the Go backend
permission gates, lightweight per-resource hooks, and two reusable display
components used across agent/skill/runtime detail pages.

- packages/core/permissions: types, rules, hooks (Decision-shaped — carries
  reason + message so UI can render disabled state, tooltip, and banner
  copy from one source)
- packages/core/agents/visibility-label: VISIBILITY_LABEL/DESCRIPTION/TOOLTIP
  constants ("Personal" / "Workspace") to replace scattered hard-coded copy
- packages/views/agents/visibility-badge: read-only visibility chip used on
  hover cards, list rows, and inspector when not editable
- packages/ui/components/common/capability-banner: "View only — only X and
  admins can edit Y" banner shown on agent / skill detail when current user
  lacks edit permission

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

* feat(views): permission-aware UI across agent/comment/runtime/skill surfaces

Apply the new permission rules to every surface where the UI was either
lying about who can do what or letting users hit 403s by clicking buttons
the backend would reject.

Agent detail
- Hide archive/restore actions for non-owner non-admin
- Replace inline editors (avatar, name, description, runtime/model/visibility/
  concurrency picker, skill-attach) with read-only display when canEdit is
  false — value is information, the editor is the action
- Show CapabilityBanner under the header explaining who can edit

Visibility surfaces
- visibility-picker / create-agent-dialog: replace "only you can assign"
  (false) with "Only you and workspace admins can assign" via shared
  VISIBILITY_DESCRIPTION constants
- agent-columns: truthful tooltip + "You" badge on agents the current user
  owns

Comments
- Restore admin override on comment edit/delete (backend already permits
  it via comment.go:507-512; the frontend was incorrectly hiding the menu).
  canModerate is computed once in issue-detail and threaded down.

Other
- Members tab: disable "demote" options for the last owner with tooltip
- Assignee picker: tooltip on disabled personal agents the user can't assign
- Runtime delete: tooltip and dialog explain the gate; owner column gains
  a name label next to the avatar in All scope
- Skill detail: page-level CapabilityBanner alongside the existing lock chip
- Issue delete (single + batch): note that any workspace member can delete
  issues — by-design semantics, made transparent

Backend is unchanged.

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

* feat(agents): hide personal agents from list and @mention for non-owners

Until now an agent's "Personal" visibility only narrowed the assign-to-issue
gate — every workspace member still saw every personal agent in the list
and the @mention dropdown. Members would see, click, and fail.

This filters those surfaces with the canonical canAssignAgentToIssue rule:
regular members only see workspace-visibility agents and the personal
agents they own; workspace owners and admins continue to see everything
(admin override path is intact).

- agents-page: visibleInView layer between active/archived and Mine/All
  scope so segment counts also reflect the filter
- mention-suggestion: filter agentItems before they enter the recency-
  ranked list; expand the test mock to cover the auth + visibility paths
  and add two assertions (member hides others' personal agents; admin
  still sees them)

Backend keeps returning every agent — admin tools and direct API access
are unaffected. This is a UI-only filter.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:31:19 +08:00
Multica Eve
b9118ae9b8 Refine Quick Create agent modal (#1879)
* fix: refine quick create agent modal

* fix: align quick create toolbar feedback

* fix: sync create mode toolbar options

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
2026-04-29 15:55:00 +02:00
Multica Eve
06880d6ba2 fix: make workspace table columns resizable (#1881)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Jiayuan Zhang <forrestchang7@gmail.com>
2026-04-29 15:23:12 +02:00
Naiyuan Qing
4ad0a0b847 feat(chat): presence v4 — status pill, failure bubble, elapsed timing (#1856)
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>
2026-04-29 18:29:46 +08:00
Bohan Jiang
9baa72cc68 fix: polish quick-create UX (kind labeling, dark toast, placeholder) (#1831)
* 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.
2026-04-29 15:40:59 +08:00
Naiyuan Qing
f745a3bbbe feat(agent): presence v3 + execution log + trigger summary (#1823)
* 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>
2026-04-29 14:50:58 +08:00
Naiyuan Qing
f0c845b777 fix: popover click bubble + resilient presence loading (#1798)
* 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>
2026-04-28 19:54:18 +08:00
Black
04f813a70f fix PR 1573 follow-up colors (#1699) 2026-04-26 11:14:40 +08:00
Naiyuan Qing
fe84e29b64 fix(ui): stop menu hover from overriding icon colors (#1612)
Menu primitives (context/dropdown/menubar/select/command) had rules like
`focus:**:text-accent-foreground` and `*:[svg]:text-destructive` that forced
descendant svg colors on focus, overriding icons that set their own color
(e.g. StatusIcon's `text-warning`). Remove them so icon color comes from
inheritance only: colored icons keep their color on hover, uncolored icons
still inherit the item's focus/destructive color as before.

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

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

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:05:33 +08:00
Bohan Jiang
c6d54e8ce5 fix(ui): replace smiley with check mark in quick emoji list (#1446)
Swap the 4th quick reply emoji 😄 for  so approval-style
acknowledgements are one tap away.
2026-04-21 17:27:40 +08:00
Naiyuan Qing
c38af55a8e refactor(ui): comprehensive UI craft review — sidebar, headers, detail panels (#1087)
Sidebar:
- Pinned items: StatusIcon for issues, emoji for projects, sm size, mask gradient text fade
- Pinned items: inline X close button (hidden → flex on hover, desktop tab pattern)
- Pinned section: collapsible with chevron + hover count
- Remove unused canvas token

Global components:
- PageHeader: shared component with built-in mobile SidebarTrigger (md:hidden)
- Replace header divs in all 11 dashboard pages with PageHeader
- Remove standalone mobile trigger bar from DashboardLayout
- Tooltip: 200ms delay, remove arrow, popover/border style
- Search dialog: add finalFocus={false}
- SidebarInset: remove shadow-sm
- Button sizing: icon-xs → icon-sm across all non-editor contexts

Issue Detail:
- Simplify breadcrumb to workspace > identifier
- Extract sidebarContent variable shared between ResizablePanel and mobile Sheet
- All sidebar sections collapsible (Properties, Parent issue, Details, Token usage)
- Auto-close sidebar on mobile breakpoint
- Collapsible section headers: text before chevron, !size-3 stroke-[2.5], hover bg

Project Detail:
- Match Issue Detail layout pattern (header inside left ResizablePanel)
- Extract sidebarContent, add mobile Sheet support
- All sidebar sections collapsible (Properties, Progress, Description)
- Header: move three-dot menu to right button group, unified breadcrumb layout
- Auto-close sidebar on mobile breakpoint

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:50:00 +08:00
Naiyuan Qing
6097f7392e refactor(chat): migrate chat input to ContentEditor + unified SubmitButton
- Replace plain textarea in chat-input with ContentEditor (rich text, matches comment-input structure)
- Extract shared SubmitButton component (idle/loading/running states) to packages/ui/components/common
- Update comment-input to use icon-sm size
- Fix chat-fab Tooltip delay prop (not supported on Root, global 500ms applies)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:52:39 +08:00
Jiayuan Zhang
a3eefcf2c4 Revert "feat: add online status indicator on agent & member avatars (#821)" (#837)
This reverts commit 1d64ea4ba6.
2026-04-13 15:03:31 +08:00
Jiayuan Zhang
1d64ea4ba6 feat: add online status indicator on agent & member avatars (#821)
* feat: add online status indicator dot on agent & member avatars

Backend:
- Track member presence via WebSocket connections in the Hub
- Broadcast member:online/offline events when users connect/disconnect
- Add GET /api/workspaces/{id}/members/online endpoint
- Add member:online and member:offline event type constants

Frontend:
- Add isOnline prop to ActorAvatar with a status dot at top-right corner
- Green dot = online, gray dot = offline, no dot = status unknown
- Fetch online member list via new query, update optimistically on WS events
- Derive agent online status from existing agent.status field
- Wire online status through ActorAvatar views wrapper (enabled by default)

* fix: address code review — fix hub tests and avatar rounding

1. Hub tests: consume the member:online presence event from the first
   connection before asserting on broadcast messages.
2. ActorAvatar: use rounded-[inherit] on the inner wrapper so callers
   can override rounding (e.g. rounded-lg for agent list items).

* fix: consume member:online presence event in integration test

Same fix as the hub unit tests — read and discard the member:online
event before asserting on issue:created in TestWebSocketIntegration.
2026-04-13 14:46:34 +08:00
Naiyuan Qing
ba32f3a187 chore: add shared ESLint config + enforce strict tsconfig across packages
- Add @multica/eslint-config package (base, react, next configs)
- Replace `next lint` (removed in Next.js 16) with `eslint .`
- Add lint scripts to all packages and desktop app
- Add noUnusedLocals, noUnusedParameters, noImplicitReturns to base tsconfig
- Fix all resulting TS/ESLint errors (unused imports, missing returns,
  stale eslint-disable comments from legacy eslint-config-next)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:27:29 +08:00
Naiyuan Qing
042985d961 fix(desktop): resolve cross-platform boundary violations and deduplicate shared code
- Extract MulticaIcon and ThemeProvider to packages/ui (remove duplication)
- Extract shared CSS (scrollbar, shiki, entrance-spin) to packages/ui/styles/base.css
- Add NavigationAdapter.openInNewTab/getShareableUrl for platform-agnostic navigation
- Fix window.open() / window.location.href in shared views to use NavigationAdapter
- Add resolve.dedupe for React in electron-vite config
- Fix desktop tsconfig (noImplicitAny: true)
- Use catalog: for all desktop dependencies
- Add shadcn + tw-animate-css to desktop dependencies (fix phantom deps)
- Add typecheck scripts to all shared packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:04:53 +08:00
Naiyuan Qing
35828492d5 feat(ui): extract packages/ui — shared atomic UI layer
- Move 55 shadcn components → packages/ui/components/ui/
- Move lib/utils.ts (cn function) → packages/ui/lib/
- Move 3 DOM hooks (auto-scroll, mobile, scroll-fade) → packages/ui/hooks/
- Extract CSS design tokens (@theme + :root + .dark) → packages/ui/styles/tokens.css
- Refactor 3 common components to pure-props (actor-avatar, mention-hover-card, reaction-bar)
- Move 6 markdown components with renderMention slot for IssueMentionCard decoupling
- Create wrapper components in apps/web/ for data-aware ActorAvatar and Markdown
- Update 116 import paths across apps/web/
- Add @source directives for Tailwind to scan packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:44:31 +08:00