Commit Graph

194 Commits

Author SHA1 Message Date
Naiyuan Qing
a590dd9a22 fix: apply working filter on project issues (#3631)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 13:07:12 +08:00
Naiyuan Qing
b72434da68 refactor(chat,issues): unify hover-swap row pattern + drop archived chat sessions (#3634)
* refactor(chat): rework chat history list

- Drop legacy archived sessions from the history dropdown. The
  soft-archive feature was removed, so status='archived' rows are dead
  data; exclude them instead of showing a collapsed "archived" group.
  Rename the section heading "Active" -> "Chat history".
- Swap hover row actions into the status column's slot instead of an
  absolute overlay: status is hidden on hover and actions take its
  place inline, while the title keeps flex-1. No mid-row gap, no
  overlap, no text bleed-through.
- Remove orphaned i18n keys (active_group, archived_group,
  archived_label) across en/zh-Hans/ko.

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

* refactor(issues): align execution log rows with the chat hover-swap pattern

- Drop the fixed w-20 status column that forced premature truncation of
  the trigger text and left a mid-row gap; status now sizes to content.
- Running tasks render only the spinner (sr-only label retained for a11y
  and tooltip); the redundant "Working" text is removed.
- Hover swaps status for actions in place (RowStatus hidden, RowActions
  inline) instead of an absolute gradient overlay. Applies to both
  active and past ("show past runs") rows via the shared RowShell /
  RowStatus / RowActions.

Known tradeoff: dropping the absolute+opacity slot also drops the
group-focus-within keyboard reveal, so cancel/retry are no longer
Tab-reachable. Matches the chat pattern; revisit if keyboard access for
row actions becomes a requirement.

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-02 10:55:02 +08:00
Naiyuan Qing
6f38891665 Improve mobile issue header controls (#3602)
* Improve mobile issue header controls

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

* Fix desktop issue header control sizing

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

* Fix issue scope tab test for mobile trigger

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

* fix(issues): keep header controls horizontally scrollable

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 16:33:30 +08:00
Naiyuan Qing
d62d894f17 fix(issues): clean execution log active rows (#3603)
* fix(issues): clean execution log active rows

* fix(issues): use fixed execution log trailing slot

* fix(issues): isolate execution log row hover

* fix(issues): keep execution log status off trigger text

* fix(issues): simplify execution log row layout

* fix(issues): preserve execution log row accessibility
2026-06-01 16:13:36 +08:00
Naiyuan Qing
8b37edbbb1 fix(issues): move agent live bar back into the activity section (#3533)
PR #3517 moved the AgentLiveCard out of the activity section to a
sticky bar above the editable title. This restores its original
placement (after LocalDirectoryHint, above the timeline) and reverts
the container margin from mb-4 back to mt-4 that suited the top slot.

Everything else from #3517 is kept: the single-container multi-agent
accordion, the lifted cancel-confirmation dialog, and the dropped
parked/running background variant.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:51:19 +08:00
Naiyuan Qing
fc9cec8e87 feat(issues): sticky agent live bar above title with multi-agent accordion (#3517)
Move the per-issue "agent is working" bar out of the activity section to a
sticky bar at the top of the main content, above the editable title, and fix
the multi-agent layout.

- Single active task fills one bordered container as a directly-actionable
  row (Logs + Stop inline).
- Multiple active tasks collapse into the same container: a summary header
  (avatar stack + count) that expands the rows inline as a divided list,
  instead of stacking N detached banners.
- Lift cancel confirmation to AgentLiveCard so one dialog serves both the lone
  row and the expanded list (avoids a confirm dialog being torn down by an
  enclosing popup).
- Drop the redundant parked/running background variant; running vs queued is
  signalled by icon + label only.
- Reuse the existing agent_activity.hover_header plural for the summary; no new
  i18n keys.
- Update tests for single-inline vs multi-accordion behavior.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:44:40 +08:00
Naiyuan Qing
645af40ed9 refactor(views): unify detail/list headers into shared BreadcrumbHeader (#3510)
* refactor(views): unify detail/list headers into shared BreadcrumbHeader

Replace four hand-rolled, divergent header styles (workspace-name root,
"/" separator, back-arrow, raw div) with one shared BreadcrumbHeader
component. The mental model is now identical everywhere: leading crumbs
are the thing's real containers and clicking one navigates up.

- New packages/views/layout/breadcrumb-header.tsx (segments/leaf/actions)
- Detail pages (issue, project, runtime, skill, autopilot, agent, squad)
  now render `{Section} › name`; org name removed as a breadcrumb root
- Issue breadcrumb shows the single most-direct container only (parent
  wins over project; they are orthogonal columns), never a fabricated
  chain; bare issue shows just its title
- Issue leaf (identifier + title) is now a clickable link to the issue
  detail page with a subtle hover:opacity-80
- Issues / My Issues list headers drop the workspace prefix, matching the
  icon + title style of the other list pages

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

* test(views): update breadcrumb tests for unified header behavior

The header unification changed three observable behaviors the tests
asserted against:
- issue detail no longer renders the workspace name as a breadcrumb root
- bare issue shows only its (now clickable) title leaf, no ancestor crumbs
- the project "Unknown project" error placeholder was removed

Rewrite the two affected issue-detail tests to assert the new leaf-link
and no-project-crumb behavior, drop the obsolete Unknown-project test, and
update the issues-page header test to assert the workspace prefix is gone.

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-05-29 14:27:51 +08:00
Dmitry Rybalka
7722a98a6a fix(ui): coalesce split task transcript messages (#3097) 2026-05-28 12:34:51 +08:00
Bohan Jiang
bed032f937 fix(issues): move local-directory hint out of the comment composer (#3363)
The "Agent will work in-place at …" banner used to render directly above
the comment input, which made it read like an input adornment. Move it
to the top of the Activity section (below the "Activity" heading, above
the live agent card) so it reads as section context instead of composer
chrome. Update the component's JSDoc and tweak the margin (mb-2 → mt-3)
to match the new placement.

MUL-2752
2026-05-27 17:21:46 +08:00
Anderson Shindy Oki
bdb60acae9 fix: swimlane empty lanes in due to pagination (MUL-2724) (#3326)
* fix: Swimlane lazy load issues

* wip

* refactor

* fix: Rebase issues

* fix: rerender

* refactor bactch and chunking
2026-05-27 16:28:15 +08:00
Alex
746c0c4456 MUL-2746 fix(avatar): normalize relative avatar urls in desktop/web (#3100)
* fix(avatar): normalize relative avatar urls in desktop/web

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

* fix: test

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

* fix(avatar): normalize avatar url in AvatarPicker preview

MUL-2746. The picker is used by create-agent and create-squad, and also
prefills from a template's `avatar_url` when duplicating an agent. The
upload result / template URL is root-relative in local-storage setups,
so on Desktop (file:// runtime) the preview <img> resolves against the
local filesystem and the avatar fails to render. Route the value through
`resolvePublicFileUrl` for rendering only; the stored URL stays raw so
the parent's create call still posts what the backend expects.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J (Multica agent) <agents@multica.ai>
2026-05-27 16:02:08 +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
Naiyuan Qing
31b58494cf feat(comments): align UpdateComment post-processing with CreateComment (#3337)
* feat(comments): align UpdateComment post-processing with CreateComment (#2965 follow-up)

Part 1 — PR #2965 code review follow-ups:
- Fix sqlc Column3 naming → AttachmentIds via sqlc.arg(attachment_ids)
- Return 500 on ReplaceCommentAttachments failure instead of logging + 200
- Remove optional marker from onEdit attachmentIds (always passed)
- Add optimistic update for attachments in useUpdateComment
- Extract useEditAttachmentState hook from CommentRow/CommentCardImpl
- Add integration tests for attachment replacement scenarios

Part 2 — Edit-comment logic alignment:
- Add ExpandIssueIdentifiers to UpdateComment (bare identifiers now expand)
- Add handleEditMentionDiff: diff old vs new agent/squad mentions on edit,
  cancel tasks for removed mentions, enqueue tasks for added mentions,
  cancel + re-trigger when content changes but mentions are unchanged

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

* fix(sqlc): regenerate with v1.31.1 + add mention diff integration tests

Fixes sqlc version downgrade (v1.31.1 → v1.30.0) that was introduced
when the original PR was authored with a local v1.30.0 binary.
Regenerated all sqlc output with v1.31.1 to match main.

Adds integration tests for handleEditMentionDiff covering: edit adds
mention → task enqueued, edit removes mention → task cancelled, edit
changes content with same mentions → cancel + re-trigger.

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

* refactor(comments): simplify edit post-processing to cancel-all + re-trigger

Replace handleEditMentionDiff (120-line mention diff) with a simpler
model: when content changes, cancel all tasks triggered by this comment,
then re-run the same three trigger paths as CreateComment (assignee,
squad leader, mentions). Fixes gap where assignee/squad-leader tasks
were not cancelled or re-triggered on edit.

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

* refactor(comments): extract triggerTasksForComment to unify Create/Edit trigger paths

Create and Edit duplicated the same three trigger paths (assignee,
squad leader, mentioned agents). A fourth path would need changes
in two places. Extract into a shared function so the composition is:
  Create: trigger() + unresolve()
  Edit:   cancel()  + trigger()
  Delete: cancel()

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 14:30:41 +08:00
Bohan Jiang
341ce7bfa5 feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)

Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.

Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.

Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.

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

* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)

Addresses the Elon review on PR #3263:

- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
  matching handler, CLI `project resource update`, and a new
  EventProjectResourceUpdated WS event. resource_type stays immutable;
  ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
  embedded label differs — the row-level UNIQUE only matches the full
  ref JSON, so a label typo would otherwise let the same working
  directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
  invalid path) and the label-shadow conflict on both create and
  update; the in-place rename still succeeds because the conflict
  scan ignores the row being edited.

Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.

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

* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)

Two follow-ups from MUL-2662 review round 2:

- CreateProject inline resources path now dedupes local_directory entries on
  (daemon_id, local_path) before opening the transaction. The DB-level
  UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
  full JSON match, so two rows with the same target but different `label`
  would otherwise slip past. Standalone POST/PUT already cover this via
  findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
  row before applying per-type shortcut flags, so `--default-branch-hint x`
  on its own no longer constructs a payload missing `url` (which the server
  400s on). Local_directory partial edits get the same merge behavior.

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

* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)

* feat(desktop): local_directory project_resource UI (MUL-2665)

First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.

What's new for the renderer:

- ProjectResourcesSection grows a desktop-only "Add local directory"
  button next to the existing GitHub-repo popover. Clicking it opens
  Electron's native folder picker, validates the path through a new
  IPC pair (existence + r/w), and submits a project_resource of
  resource_type=local_directory with daemon_id pulled live from
  daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
  greys out when ref.daemon_id != this machine's daemon_id (with a
  "only available on the machine that registered this directory"
  tooltip). Delete stays enabled so users can drop stale registrations
  from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
  shows "Agent will work in-place at {label} ({path})" when the issue's
  project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
  that the daemon will publish when it dequeues a task but can't
  acquire the path lock. The render is in place now so the daemon
  sibling subtask can wire the status string without an additional UI
  PR.

Plumbing:

- @multica/core/types gains LocalDirectoryResourceRef +
  UpdateProjectResourceRequest, and the api client gets the matching
  PUT method backed by the server endpoint that landed in
  2ac3faebb (MUL-2662). A useUpdateProjectResource hook drives the
  in-place label edit.
- New Electron handlers under apps/desktop/src/main/local-directory.ts:
    local-directory:pick     -> dialog.showOpenDialog (openDirectory)
    local-directory:validate -> stat + access(R_OK + W_OK)
  exposed through the preload as desktopAPI.pickDirectory /
  validateLocalDirectory. View code talks to them via a thin
  packages/views/platform helper that returns reason=unsupported on
  web instead of crashing.
- useLocalDaemonStatus exposes the local daemon's id, device name, and
  running flag from daemonAPI.onStatusChange so the renderer can do the
  cross-device match without coupling to the desktop preload typings.

Tests:

- pickStageKeys gets a unit test covering the new stage and proving
  the directory-release status outranks availability hints.
- LocalDirectoryHint tests cover the four render branches (no project,
  no daemon, foreign daemon, matching daemon).
- i18n parity stays green; new keys added under projects.resources.*
  and chat.status_pill.stages.waiting_for_directory_release in both
  locales.

Out of scope (will land separately):
- The daemon-side waiting/lock signal that flips the pill into the
  new state.
- Adding local_directory to the create-project modal's bulk
  attach flow.
- Docs page refresh for project-resources.mdx — left for the
  MUL-2618 umbrella sweep.

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

* fix(desktop): hide rename for foreign daemon local_directory rows (MUL-2618)

Address review nit on #3273: the rename pencil was gated only by
`canEdit`, so a foreign / unknown-daemon row still showed it even
though the spec says cross-device rows are disabled. Gate rename on
`!mismatch` so it disappears on those rows; delete stays available
so a stale registration can still be dropped from any device.

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

---------

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

* feat(daemon): local_directory execution + path mutex + GC exception (MUL-2663) (#3274)

* feat(daemon): local_directory execution + path mutex + GC exception (MUL-2663)

Wires up the daemon side of the local_directory project_resource introduced
in MUL-2662. When a task is dispatched against a project whose resources
include a local_directory pinned to this daemon's UUID, the daemon now:

  - Validates the path (absolute, exists, daemon process can read+write,
    not in the system-root / $HOME blacklist) and fails the task fast on
    any precondition violation, with a user-readable reason.
  - Serialises concurrent tasks on the same on-disk path via a
    daemon-local LocalPathLocker keyed by symlink-resolved realpath. The
    lock is held for the entire task lifetime (claim → context write →
    agent → result report).
  - When the lock is contended, the daemon flips the row to a new
    waiting_local_directory status on the server (carrying a wait_reason
    like "<path> (held by task <short id>)") so the UI can render
    "等待本地目录释放" instead of leaving the row silently in dispatched
    past the sweeper timeout. The status accepts being woken into running
    once the lock is acquired.
  - Sets execenv.WorkDir to the user's path (no copy, no mount). envRoot
    still lives under workspacesRoot/<wsID>/ and hosts output/, logs/, and
    .gc_meta.json — the daemon's logbook for the run.
  - Stamps GCMeta.LocalDirectory=true so the GC loop never RemoveAlls
    envRoot for these tasks (gcActionClean → gcActionCleanArtifacts,
    gcActionOrphan → gcActionSkip). The user's directory was never under
    envRoot to begin with, so this is defense in depth.
  - Skips execenv.Reuse for local_directory tasks because the prior
    WorkDir is the user's path and reusing it through that code path
    loses the envRoot association the GC loop needs. Prepare is cheap
    here (no clone, no copy), so always running it is fine.

Server-side protocol changes:

  - New CHECK value 'waiting_local_directory' on agent_task_queue.status
    plus a wait_reason TEXT column (migration 109).
  - All cancel / active / counted-as-running / orphan-recovery queries
    expanded to include the new status; FailStaleTasks intentionally
    excludes it (the daemon owns the wait).
  - New SQL MarkAgentTaskWaitingLocalDirectory(id, reason) and a relaxed
    StartAgentTask that accepts both dispatched and
    waiting_local_directory as preconditions (and clears wait_reason on
    the way through).
  - New POST /api/daemon/tasks/{taskId}/wait-local-directory endpoint,
    TaskService.MarkTaskWaitingLocalDirectory broadcaster, and matching
    daemon Client.MarkTaskWaitingLocalDirectory.

Tests cover: path blacklist + R/W enforcement, mutex serialisation +
ctx-cancelled wait, lock handover between two tasks, GC never returns
gcActionClean / gcActionOrphan for local_directory rows (with negative
control for the standard path), and Prepare/Cleanup correctly substitute
+ protect the user's WorkDir.

The desktop UI side (UI for adding a local_directory resource, surfacing
the "等待本地目录" badge) is MUL-2665; the agent-task lifecycle changes
(no branch switch, dirty-tree tolerant, auto-commit) are MUL-2664.

This PR targets the shared MUL-2618 v1 feature branch agent/j/912b8cb1,
not main; the whole v1 will be merged to main together when complete.

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

* fix(daemon): tighten local_directory status, symlink, cancel handling (MUL-2618)

Address the 3 must-fix items from Elon's review of PR #3274.

1. Status string unified. The server / daemon publish
   `waiting_local_directory`; align views, locales, and the
   pickStageKeys test (PR #3273 had used `waiting_for_directory_release`
   on a placeholder string). Without this, the daemon's wait state
   never reached the pill once the two siblings merged.

2. validateLocalPath now also runs the blacklist against the
   symlink-resolved realpath, with macOS's `/etc` -> `/private/etc`
   redirect handled via `isBlacklistedRealPath` which compares
   canonical forms. Without this, a symlink such as
   `/Users/me/proj/home -> /Users/me` slipped the literal $HOME check
   while every daemon write still landed in the user's home. Tests
   cover symlink-to-home, symlink-to-system-root, and the negative
   case (symlink to a regular subdirectory).

3. acquireLocalDirectoryLockIfNeeded now spins up a cancellation
   watcher inside `onWait` (lazy — the fast path stays free) so the
   gap between dispatch and StartTask responds to server-side cancel
   or row deletion. If the watcher fires while the daemon is parked
   on the path mutex, the lock-wait context is cancelled, Acquire
   returns promptly, and the helper exits silently the same way the
   run-phase poller does. New TestAcquireLocalDirectoryLock_CancelDuringWait
   exercises the path end-to-end with a fake server.

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

* fix(daemon): unconditional canonical blacklist + Windows drive-root generalisation (MUL-2618)

- validateLocalPath now always runs isBlacklistedRealPath on the
  symlink-resolved path, not only when it differs from absPath. The old
  guard let users type the canonical form of an OS-symlinked banned root
  (e.g. /private/tmp, /private/etc, /private/var on macOS) straight
  through, since EvalSymlinks is a no-op on already-canonical input.
- Windows drive-root rejection moved off the static C/D/E/F enumeration
  onto filepath.VolumeName via a new isDriveRoot helper, so removable /
  network drives mounted at G:..Z: and UNC \\server\share roots are also
  blocked. systemRootBlacklist keeps the well-known C:\ trees only.
- Tests: macOS-only case exercises direct /private/{tmp,etc,var}; a
  new TestIsDriveRoot covers the Windows generalisation (skipped on
  POSIX runners by runtime guard).

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

---------

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

* feat(views): wire waiting_local_directory end-to-end in issue UI + presence (MUL-2618)

Connect the daemon-emitted `task:waiting_local_directory` and `task:running`
events through to issue execution log, sticky agent banner, activity indicator,
and agent presence so a parked task is no longer invisible on the issue page.

- Add `waiting_local_directory` to `AgentTask.status` and the typed
  `task:running` / `task:waiting_local_directory` WS event payloads.
- Chat realtime sync writes both new statuses into the pending-task cache so
  the chat StatusPill flips out of a stale `dispatched` frame.
- ExecutionLogSection: count `waiting_local_directory` as active, add tone +
  status label, treat parked tasks the same as dispatched for time anchor /
  transcript visibility / terminate-confirm note.
- AgentLiveCard: subscribe to both new events, rank the parked state between
  dispatched and queued, and surface a "is waiting for the local directory"
  banner with the muted "Clock" treatment used for queued.
- IssueAgentActivityIndicator: route parked tasks into the queued bucket so
  the hover stack and chip stay visible.
- derive-presence: parked tasks count toward `queuedCount` so the agent
  workload chip stays out of `idle` while the daemon waits on the path lock.
- Locales: add `agent_live.is_waiting_local_directory` and
  `execution_log.status_waiting_local_directory` (en + zh-Hans).

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

* feat(project): enforce one local_directory per (project, daemon) (MUL-2618)

The daemon-side resolver picks the first matching local_directory by
daemon_id, so allowing two rows on the same daemon — even at different
paths — let the agent silently write into whichever sorted first. Tighten
the invariant top to bottom:

- server: `findLocalDirectoryConflict` rejects any second row sharing a
  daemon_id, regardless of `local_path` or label. Bundled-create surface in
  `CreateProject` runs the same daemon-scoped dedupe up front.
- daemon: `findLocalDirectoryAssignment` fails fast when it finds more than
  one row pinned to the current daemon (older API client / direct DB
  writes can still produce that state — refuse to guess).
- desktop UI: hide the "Add local directory" action once the current
  daemon owns a row on this project, with a hint and a defensive toast on
  the call path; foreign-daemon rows stay visible read-only as before.
- Tests:
  * daemon: new `two local_directory rows on this daemon fail fast` /
    `local_directory rows on different daemons coexist` cases.
  * handler: rewrite the legacy `LabelShadow` cases as
    `DaemonScopedConflict` / `BundledLocalDirectoryDaemonConflict` —
    asserts 409 on same-daemon different-path, 201 on per-daemon bundles.
- Locales: en + zh-Hans copy for the new hint + toast.

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

* chore(sqlc): drop stale skills_local in UpdateAgentCustomEnv (MUL-2618)

Follow-up to the main-merge in 0f8e8ca7: the auto-merge preserved most
of main's skills_local revert but kept the column reference inside the
UpdateAgentCustomEnv scanner because that block hadn't been touched by
either side. Re-running `sqlc generate` regenerates the file without
skills_local in this query, matching the rest of the file and the
post-revert schema.

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

* feat(create-project): binary source picker — repos OR local directory

Turn the create-project dialog's "Repos" pill into a binary Source
picker. A project's source is mutually exclusive: either a set of
GitHub repos (worktree mode, default) or a single local working
directory (local mode, desktop-only). Mirrors the constraint the
backend will enforce next.

Behavior:
- Pill shows the active mode's selection (GitHub icon + repo count, or
  folder icon + local label/path).
- Popover has a 2-tab segmented control at the top; the Local tab is
  hidden entirely on web (local_directory needs a daemon_id).
- Local tab requires the daemon online — amber notice + disabled picker
  when offline, re-renders automatically via useLocalDaemonStatus.
- Switching tabs preserves the other side's stash, but handleSubmit
  only emits the resource matching the active sourceMode, so abandoned
  picks never leak into the created project.

Backend mutual-exclusion validation + the resources-section
conditional-add-button still to come — this PR just unblocks the
dialog so it can be demoed.

* fix(mobile): cover waiting_local_directory in run row status maps (MUL-2618)

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Multica J <j@multica.ai>
2026-05-27 13:44:31 +08:00
Naiyuan Qing
2af2f7b3e4 fix(comments): defer input clearing until submit succeeds (#3344)
clearContent() and setIsEmpty() were called before await onSubmit(),
causing permanent content and draft loss on network failure. Move both
to the success path, consistent with attachment and draft cleanup.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 13:42:22 +08:00
Naiyuan Qing
6261e2b6b2 fix(comments): clear editor immediately on submit to eliminate WS race glitch (#3331)
* fix(comments): clear editor immediately on submit to eliminate WS race visual glitch

The comment editor stayed populated while WebSocket delivered the new
comment faster than the HTTP response, causing a "duplicate comment"
flash. Move clearContent/setIsEmpty before the await so the editor clears
at click time. Also remove dead `submitting` state in useIssueTimeline
(redundant with the input components' own guards) and dead `isTemp` logic
in comment-card (no code path ever creates temp- prefixed entries).

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

* fix(comments): preserve attachments on submit failure and fix CommentRow indentation

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 10:39:21 +08:00
李冠辰
963ed5cd0e feat(comments): allow selecting multiple attachments 2026-05-27 10:13:27 +08:00
YOMXXX
7d24a8594a fix(comments): support edit-time attachment removal (#2965) 2026-05-27 09:48:59 +08:00
Naiyuan Qing
730fb61f4a fix(views): keep sort label centered in viewport during board scroll (#3325)
The "Board ordered by" overlay used absolute positioning inside a
scrollable container, causing it to drift with scroll content. Move
the overlay outside the scroll area into a non-scrolling wrapper so
it stays centered in the visible viewport.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 09:00:08 +08:00
Bohan Jiang
fa2a0e57ec feat(views): swimlane supports parent / project / assignee grouping (MUL-2711) (#3311)
* feat(views): swimlane supports parent / project / assignee grouping (MUL-2711)

The swimlane view was hard-coded to group by parent issue. This adds a
display dropdown so users can pick parent (default), project, or
assignee — analogous to how the board view exposes its grouping option.

- Generalise the lane builder in swimlane-view.tsx behind a `LaneGroup`
  abstraction (matcher + per-grouping `moveUpdates` payload) so the
  drag-end handler no longer branches on grouping. Cell ids gain a
  `<grouping>:<rawId>` prefix and lane sortable ids include the
  grouping so dnd-kit cannot collide entries from different groupings.
- Extend the view store with `swimlaneGrouping`, `swimlaneOrders` (one
  saved order per grouping), and a grouping-keyed `collapsedSwimlanes`.
  The persist `merge` defends against the old `string[]` shape so a
  pre-upgrade snapshot doesn't crash on first read.
- Wire `setSwimlaneGrouping` into the issues display popover next to
  the existing board grouping control. Add en / zh-Hans copy for the
  three swimlane buckets (Parent issue / Project / Assignee) and the
  two new pinned lanes (No project / Unassigned).
- Expand swimlane tests with parent / project / assignee smoke cases
  and update existing mocks to the new lane-id format. Add stable
  `useActorName` / `projectListOptions` mocks to avoid the
  set-state-in-effect loop that an unstable `getActorName` would
  trigger via the cells-rebuild memo.

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

* feat(views): default swimlane grouping to assignee

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

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 22:23:14 +08:00
Naiyuan Qing
30a7da6d10 feat(list): drag-and-drop reordering + loadMore sort fix (#3284)
* feat(list): drag-and-drop reordering + shared drag utils

- List view drag-and-drop: reorder within/across status groups, drop into
  collapsed groups, DraggableListRow with useSortable + checkbox protection
- Extract shared drag utilities (computePosition, findColumn, buildColumns,
  makeKanbanCollision, issueMatchesGroup, getMoveUpdates) to drag-utils.ts
- Reuse IssueDisplayControls in MyIssues header (dedup ~380 lines)
- Radio check marks for grouping/sort dropdowns
- Remove outer p-1 wrapper, unify board view to p-2

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

* fix(my-issues): move MyIssuesHeader inside ViewStoreProvider

IssueDisplayControls calls useViewStore which requires the provider context.

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

* fix(header): restore swimlane option in view toggle dropdown

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-05-26 17:09:42 +08:00
Bohan Jiang
057cb2ff34 fix(issues): thread sort through load-more so appended pages render (MUL-2678) (#3279)
The recent server-side-sort change (#3228) keyed the issue-list cache by
sort but did not update the load-more hooks: useLoadMoreByStatus used a
prefix-match that could pick a stale cache variant, and neither hook
forwarded sort to the API request. As a result, scroll-to-load-more
fired its request, but the response was either appended to a cache no
useQuery was subscribed to, or it appended rows in an unsorted order
into a sorted bucket.

Pass `sort` explicitly through Board/List/Swimlane and into the hooks.
The hook now targets the full sorted key via setQueryData and forwards
sort to the listIssues / listGroupedIssues calls so the appended page
lines up with the existing items.

Also adds focused tests for both load-more hooks: stale-sort cache is
untouched, sort is forwarded to the API, and sort-less callers still hit
the {} key path used by actor-issues-panel.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 16:27:33 +08:00
Anderson Shindy Oki
65ed5744a6 feat: add issue view swimlane (MUL-2607) (#3149)
* feat: Add swimlane view

* refactor

* test: add tests

* wip

* wip

* wip

* refactor

* refactor and fixes

* refactor

* fix: parent empty cells

* chore: Remove meanless comments

* remove meanless comments
2026-05-26 15:22:16 +08:00
Naiyuan Qing
612ac8f28e feat(issues): server-side sort + fix drag position corruption (#3228)
* refactor(editor): split rich text styles

* feat(issues): server-side sort + fix drag position corruption in non-manual sort

Backend: ListIssues and ListGroupedIssues now accept `sort` and `direction`
query params (position/priority/title/created_at/start_date/due_date).
ListIssues converted from sqlc to hand-written SQL for dynamic ORDER BY.
Priority sort uses CASE expression for semantic ordering.

Frontend: query keys include sort so changing sort triggers server refetch.
Client-side sortIssues() removed from board-view and list-view.

Drag-and-drop: non-manual sort disables within-column reorder (prevents
silent position corruption). Cross-column drag only updates status/assignee,
preserves original position. Column overlay shows current sort during drag.

Cache: query key split into prefix (list) for invalidation and full key
(listSorted) for queryOptions. All optimistic update paths use prefix
matching via getQueriesData to work with any active sort.

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

* fix(board): prevent drag flicker by settling columns until mutation refetch

After drag-and-drop, the optimistic cache patch updates position values
without reordering the bucket array. The useEffect that rebuilds columns
from TQ data would overwrite the correct local drag order, causing cards
to snap back then forward. Fix: isSettlingRef blocks column rebuilds
between drag end and mutation onSettled.

Also invalidate issueKeys.list on WS position changes so other windows
refetch correctly sorted data instead of showing stale bucket order.

Includes debug logs (to be removed after verification).

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

* fix(board): stabilize drag-and-drop for non-manual sort modes

Three behavioral fixes for board drag when sort != position:

1. Settling: isSettlingRef + settleVersion blocks column rebuilds
   between drag-end and mutation settle, preventing the optimistic
   cache patch (which updates position values without reordering the
   bucket array) from overwriting the correct local column state.

2. Non-manual cross-column: handleDragOver returns prev (no visual
   card movement — column highlight + sort label is sufficient).
   handleDragEnd uses overCol directly instead of findColumn on the
   card's current position (which would be the source column).
   Cards use useSortable({ disabled: { droppable: true } }) to
   suppress within-column insertion indicators.

3. Collision detection: when no card droppables exist (disabled in
   non-manual sort), return column droppables from pointerWithin
   instead of falling through to closestCenter, so isOver reflects
   the column the pointer is actually inside.

Also: WS position changes now invalidate issueKeys.list so other
windows refetch correctly sorted data.

Insertion-position prediction intentionally omitted — PostgreSQL's
en_US.utf8 collation (glibc) cannot be faithfully replicated in
JavaScript (ICU/V8), and an inaccurate indicator is worse than none.

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

* fix(sort): manual sort ignores direction param on both ends

Manual sort (position) is user-defined order via drag-and-drop —
reversing it has no product meaning.

Backend: sort=position now skips the direction query param and
always uses ASC. Both ListIssues and ListGroupedIssues handlers.

Frontend: sort object omits sort_direction when sortBy is position.
Direction toggle hidden in the display popover for manual mode.

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

* perf(board): memo columns + stabilize references to reduce re-renders

- BoardColumn, PaginatedBoardColumn, PaginatedAssigneeBoardColumn
  wrapped in memo() — only columns with changed props re-render
- IssueAgentActivityIndicator wrapped in memo() — 111 snapshot
  subscribers no longer trigger full re-render on every WS task event
- buildColumns rewritten from O(groups × issues) to single-pass O(n)
- EMPTY_IDS constant replaces ?? [] fallbacks (stable reference)
- EMPTY_CHILD_PROGRESS constant replaces new Map() default
- BOARD_COL_WIDTH / BOARD_CARD_WIDTH constants shared between
  column and DragOverlay for consistent card dimensions
- issueListOptions + issueAssigneeGroupsOptions use
  placeholderData: keepPreviousData so sort/filter changes don't
  flash a full-page skeleton
- Loading skeleton scoped to content area only — header stays
  rendered during data transitions

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

* chore: remove outdated server-side sort implementation plan

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-05-26 14:50:25 +08:00
Naiyuan Qing
312ee29cb6 fix(views): always show assignee meta row on board cards when assignee property is enabled (#3250)
When no assignee was set, the entire meta row (assignee + dates + child progress) could disappear because showAssignee required both storeProperties.assignee AND issue.assignee_id. Now the row visibility depends only on storeProperties.assignee, and unassigned cards show "Unassigned" text with a clickable picker to assign.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 10:09:00 +08:00
Naiyuan Qing
e33b893c3f feat(views): sticky group headers in list view (#3221)
* feat(views): add sticky positioning to list-view group headers

Group headers now stay pinned at the top of the scroll viewport so users
always know which status group they are looking at. Background changed
from semi-transparent to opaque to prevent content bleeding through.

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

* fix(views): remove top padding from list-view scroll container for sticky headers

The `p-2` padding on the scroll container caused an 8px gap above sticky
group headers. Replace with `px-2 pb-2` to keep horizontal and bottom
padding while allowing headers to stick flush to the top. Sync skeleton
containers in issues-page and my-issues-page to match.

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

* fix(views): use p-2 pt-0 instead of px-2 pb-2 for list-view scroll container

Reporter preferred adding pt-0 to override the top padding from p-2,
keeping the original p-2 shorthand intact.

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

* fix(views): opaque sticky header hover + cursor-pointer on trigger

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-26 08:27:57 +08:00
Bohan Jiang
636fa1adb4 fix(issues): hide activity-block header while only recent entries are shown (#3226)
In the trailing activity block's default truncated state ("last 8 shown,
N older hidden"), we were rendering two stacked chevron rows: a "v N
activities" collapse header and a "> Show N more activities" reveal link.
Visually that looked like nested folds even though they're parallel
controls, and the header is redundant when the user just wants a glance
at recent activity.

Drop the header in the truncated default state. It reappears the moment
the user clicks "Show N more" — at that point they're seeing the full
block and a fold-back affordance becomes useful again. Blocks that fit
within the 8-entry limit (and non-trailing blocks, which never truncate)
keep their header as before.
2026-05-25 19:04:15 +08:00
Bohan Jiang
441fa18db4 feat(issues): truncate trailing activity block to most recent 8 (MUL-2628) (#3219)
* feat(issues): truncate trailing activity block to most recent 6 (MUL-2628)

The trailing activity block defaults to expanded, but a block with dozens
of entries still drowns the comment area. Show only the most recent 6 by
default; older entries fold behind an in-place "Show N more activities"
toggle. Non-trailing blocks are unchanged — they still collapse whole.

The "show older" choice is tracked per block id in a separate Set so it
survives the block losing its trailing position (when a new comment
lands after it) and survives a collapse/re-expand cycle.

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

* refactor(issues): bump trailing activity block visible limit from 6 to 8 (MUL-2628)

User feedback on the original PR: 6 felt slightly too tight. Bumped the
trailing-block truncation threshold to 8 entries to give the "most recent
activity" view a bit more headroom before older entries fold behind the
"Show N more activities" toggle.

Test count is unchanged; the existing trailing-block / non-trailing-block
truncation cases were adjusted to exercise the new 8-entry boundary
(10-entry trailing block → 2 hidden; 8-entry trailing block → none
hidden; 10-entry non-trailing block → all visible after expand).

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-25 18:43:31 +08:00
Bohan Jiang
13f74e651a feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600) (#3209)
* feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600)

The agent resource shape (list / get / create / update / archive /
restore responses + WebSocket events) no longer carries `custom_env`
values. Reads/writes of env now flow exclusively through a dedicated
`/api/agents/{id}/env` endpoint that is owner/admin-only, rejects
agent-actor sessions, applies a "****" sentinel preserve guard on
PUT, and writes a persistent audit row per reveal/update.

Why
- `multica agent list --output json` historically returned plaintext
  `custom_env` for owner/admin callers (the redaction gate gave only
  members the masked map). Any agent token running on the workspace
  inherits its owner's role and could read every other agent's
  secrets just by listing.
- Patching list/get redaction alone (PR #3175 direction) left
  symmetric leaks via mutation responses, WS events, the "reveal"
  path itself (no actor-aware auth), and a `****` overwrite footgun
  on UpdateAgent.

What changed
- Backend: drop `custom_env` from AgentResponse; add coarse
  `has_custom_env` + `custom_env_key_count`. Strip env handling from
  UpdateAgent (silently ignored if sent). Keep CreateAgent's
  custom_env acceptance.
- Backend: new GET/PUT `/api/agents/{id}/env` handlers in
  `internal/handler/agent_env.go`:
  - resolveActor → 403 for agent actors (closes the lateral-movement
    path).
  - Owner/admin role gate via existing helper.
  - PUT honours value == "****" as "preserve existing value".
  - Both write to `activity_log` with `agent_env_revealed` /
    `agent_env_updated` actions. Audit details record key names only,
    never values.
- Daemon claim path (`ClaimAgentTask`) unchanged — `TaskAgentData`
  still carries plaintext env for runtime injection.
- SQL: new `UpdateAgentCustomEnv` query; sqlc regenerated (v1.31.1).
- CLI: new `multica agent env get|set` subcommands. `--custom-env*`
  flags removed from `multica agent update`; the no-fields error
  now points to the new path.
- Frontend: drop env fields from `Agent` + `UpdateAgentRequest`; add
  `getAgentEnv` / `updateAgentEnv` client methods; rewrite env-tab
  to show "N variables configured" + explicit "Reveal & edit"
  button, fetching values only on intentional reveal.
- Locales: parity-safe additions to en + zh-Hans.
- Docs: agents-create.{mdx,zh.mdx} reflect the new threat model and
  endpoint.
- Mobile: schema drops `custom_env` / `custom_env_redacted`, adds
  metadata fields.

Tests
- Handler tests pinned the new invariants: no env in list/get
  responses, owner reveal happy-path + audit row, agent-actor 403,
  `****` sentinel preserves real values, UpdateAgent silently
  ignores `custom_env`, pure `mergeAgentEnv` cases.
- CLI tests pivot to the new flag surface: `agent update` MUST NOT
  expose the env flags; `agent env set` MUST expose
  --custom-env-stdin/--custom-env-file.
- Frontend test fixtures updated; pnpm typecheck / test / lint
  pass cleanly.

This is a breaking API change. Scripts that read `custom_env` from
`/api/agents` must migrate to `GET /api/agents/{id}/env`.

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

* fix(agents): close actor-spoofing + audit fail-closed in env endpoints (MUL-2600)

Addresses Elon's review of #3209:

* Mint a task-scoped `mat_` token per claim, bound to (agent, task,
  workspace, owner). Daemon injects it into the agent process in place
  of its own credential. Auth middleware authoritatively rebuilds
  X-User-ID / X-Agent-ID / X-Task-ID from the token row and sets
  X-Actor-Source=task_token; that header is server-set only — incoming
  values are stripped before any auth branch runs. resolveActor honors
  the header so an agent that strips X-Agent-ID / X-Task-ID still
  resolves as actor=agent.
* GetAgentEnv / UpdateAgentEnv are now fail-closed on audit-log
  failures: GET refuses to return plaintext, PUT persists inside the
  same tx as the audit row so they commit/roll back together.
* PUT /api/agents/{id} returns 400 when the body carries custom_env
  instead of silently dropping it — directs callers to the audited env
  endpoint.
* Agent actors never see mcp_config, even when the underlying member
  is owner/admin; mutation broadcasts go through a redaction shim so
  WS subscribers don't pick it up either.
* Fix backend test that asserted dense JSON (jsonb::text renders
  whitespace) and frontend test that assumed a unique "Test User"
  match.

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

* fix(agents): close residual MUL-2600 gaps from review (MUL-2600)

Migration 108 FK now correctly references agent_task_queue(id) instead
of the non-existent agent_task table; the previous name blocked CI
backend migrations.

Task-token-authenticated requests can no longer be re-routed at a
different workspace by passing workspace_slug / workspace_id /
?workspace_id / a URL workspace param. ResolveWorkspaceIDFromRequest
and resolveWorkspaceUUID both short-circuit on X-Actor-Source=task_token
and return only the token-bound X-Workspace-ID; buildMiddleware adds a
defence-in-depth 403 if any URL-resolved workspace disagrees with the
token binding.

mcp_config no longer leaks back to agent actors through UpdateAgent /
CreateAgent / ArchiveAgent / RestoreAgent HTTP responses — the same
redactAgentResponseForActor helper that GetAgent/ListAgents use is now
applied to mutation responses too. WS broadcasts were already redacted
via broadcastAgentResponse.

FailTask and every TaskService cancel path (CancelTask /
CancelTasksForIssue / CancelTasksForAgent / CancelTasksByTriggerComment
/ BroadcastCancelledTasks) now eagerly DeleteTaskTokensByTask so the
mat_ token's 24h window doesn't outlive a terminated task. Failure is
non-fatal — the FK cascade and expiry remain durable guards.

Doc-only: clarify that PUT /api/agents/{id} now hard-rejects bodies
that carry custom_env (was previously "silently ignores").

Tests:
- middleware: TestResolveWorkspaceIDFromRequest gains a task_token
  case asserting client-supplied slug/id/query cannot override the
  bound workspace.
- handler: TestUpdateAgent_RedactsMcpConfigForAgentActor and
  TestUpdateAgent_KeepsMcpConfigForMemberActor pin the mutation-
  response redaction contract per actor type.

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

* fix(agents): match redacted mcp_config as JSON null, not Go nil (MUL-2600)

`AgentResponse.McpConfig` is `json.RawMessage` without `omitempty`, so
the redacted response serialises as `"mcp_config": null`. On decode,
`json.RawMessage` keeps the literal bytes `null` rather than collapsing
to Go nil, which made the assertion fire on a non-leak.

The product contract (field always present, distinguished from "no
config" via `mcp_config_redacted`) is intentional, so adjust the test
to check for "no secret-bearing content" instead of weakening the
contract via `omitempty`.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-25 18:42:48 +08:00
Bohan Jiang
90455abd8d fix(desktop): preserve tab scroll position across Activity visibility cycles (MUL-2602) (#3196)
Closes #3183.

Tabs render under `<Activity mode="visible|hidden">`, which keeps React
state but drops DOM scrollTop when the subtree leaves layout. Switching
to another tab and back sent users to the top of long discussions.

`useTabScrollRestore` records the scrollTop of every element marked with
`data-tab-scroll-root` while the tab is visible (capture-phase scroll
listener) and restores them in a useLayoutEffect on the next visible
transition, before paint. Saved offsets are dropped when the tab's path
changes so intra-tab navigation lands at scroll=0 instead of inheriting
the previous route's position.

Mark scroll containers in views with `data-tab-scroll-root` (issue
detail + chat message list ship with the marker; other views can adopt
the convention as needed).

`useAutoScroll` previously called `scrollToBottom()` on every effect
mount, which would have overwritten the restored offset every time a
chat tab cycled back to visible. Guard it with a once-per-instance ref.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-25 15:31:01 +08:00
Naiyuan Qing
6261ea45fd Improve board and squad hover cards (#3188) 2026-05-25 12:58:39 +08:00
Bohan Jiang
f2e6dc75bd feat(create-issue): collapse start date into ⋯ overflow menu (#3063)
Start date is a low-frequency field for most issues, so the always-on
inline pill was crowding the property toolbar. Move it behind the ⋯
overflow menu by default: the pill only appears once a value is set,
or transiently while the calendar popover is open after the user picks
"Set start date..." from the menu. Closing the popover without a value
returns the pill to the menu-only state.

To make the menu item open the popover programmatically, lift the
picker's open state via new controlled `open` / `onOpenChange` props
(matching the priority-picker pattern).

MUL-2557

Co-authored-by: multica-agent <github@multica.ai>
2026-05-22 16:13:23 +08:00
Naiyuan Qing
e0b756f515 feat(issues): redesign board card layout + extract useTimeAgo i18n hook (#3064)
* refactor(views): replace static timeAgo with shared useTimeAgo hook

The previous timeAgo helper in packages/core/utils.ts hardcoded English
output ("2d ago"), producing "更新于 2d ago" mixed-language strings in
zh locale. Replaced with a localized useTimeAgo() hook in
packages/views/i18n, backed by common.time.{just_now,minutes_ago,
hours_ago,days_ago} translation keys. Migrated all 10 view-side
call sites and removed the static function.

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

* feat(issues): redesign board card layout

Properties were piling onto the bottom row (assignee + priority badge
+ start date + due date) until it overflowed. Restructured into four
semantic rows:

- Top: priority icon (left, icon-only — color already conveys urgency)
  + identifier; agent activity indicator (right)
- Title
- Chip row: project + labels
- Meta row: assignee (left, avatar + name when only property present;
  bare avatar otherwise) + start/due dates + child progress

Long agent/team names truncate cleanly (min-w-0 + max-w-[160px]) and
dates/progress are shrink-0 so they never compress. When the meta row
contains only an assignee, the right side fills with "Updated 2d ago"
to avoid a half-empty 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-05-22 15:21:10 +08:00
Naiyuan Qing
fedd0f1694 feat(issues): live agent activity chip + per-issue indicator + filter (#3058)
* feat(server): broadcast task:running event

The dispatched → running transition was silent: only task:queued,
task:dispatch, task:cancelled, task:completed and task:failed
broadcast over WS. Any UI that distinguishes "queued" from "running"
(e.g. the new issue-card agent activity indicator) would lag by up to
the 30s agentTaskSnapshot staleTime on the most user-visible
transition. StartTask now broadcasts task:running so the workspace
snapshot invalidates immediately, keeping the agent activity UI live.

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

* feat(issues): live agent activity chip + per-issue indicator + filter

Surfaces "which agents are working on what, right now" in the Issues
and My Issues views, with a one-click filter to narrow the list to
issues that have a running agent task.

Two visual surfaces:

- **Workspace chip** in the header (left of Filter). Shows the
  brand-tinted avatar stack of agents currently running on visible
  issues. Click toggles a page-scoped filter; idle state renders a
  static "0 working" button with a hover-card placeholder. When the
  filter is active the chip pins to brand fill across hover and popover
  states (the Button outline variant otherwise repaints back to
  neutral). A muted "Viewing only working agents" hint sits to the
  left of the chip whenever the filter is on, so users notice the
  active state without having to hover.

- **Per-issue indicator** on every board card and list row (top-right
  of the identifier line). Renders the avatar stack of agents in
  running or queued state on that issue, full-opacity ring at brand/70
  when ≥1 is running, half-opacity stack when only queued. Returns
  null when nothing is in flight.

Both surfaces open the same hover-card body that lists each active
task with the agent avatar, status dot (composed via the existing
availability + workload tokens), and a live-ticking duration.

Adds a new "All" scope to /my-issues that unions assignee, creator,
and involves_user_id via three parallel fetches deduped on the
client — no backend changes for this part. The chip's count and the
quick-filter both use the page's currently visible issue ids so they
stay in sync with the active scope.

State is per-user (Zustand + localStorage) and the agentRunningFilter
is intentionally omitted from partialize — running state changes
second-to-second and a stored toggle would land users in an
unexplained empty list. WS task:running, already added in the
preceding commit, drives real-time updates without polling.

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

* refactor(issues): swap indicator ring pulse for shimmer text label

Earlier iterations layered a brand ring with various opacity-pulse
cadences around the per-issue avatar stack. Every tuning attempt was
either invisible (transparent ring + faded pulse) or oppressive (a
visible ring that flashed on a dense board). Moves the "alive" signal
onto a small text label and reuses chat's existing
`animate-chat-text-shimmer` utility — a soft light sweep across the
glyphs that already powers the ChatGPT-style "thinking" cue in
task-status-pill.

Indicator now reads as a 12 px avatar stack + 10 px label:

- Running → full-opacity avatars + shimmering localized "Working"
- Queued  → half-opacity avatars + muted static "Queued"
- Idle    → render nothing (unchanged)

Avatars and the surrounding card stay completely still; only the few
glyphs animate. The label is i18n-driven via the existing
`status_running` / `status_queued` keys, so no locale changes are
required.

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-05-22 14:20:42 +08:00
DorothyWoolf
a5582198ab fix(views): widen assignee picker and add text truncation (#2947) (#3044)
Popover was too narrow (w-52) to display long names. Widened to w-64 and
added truncate class to member/agent/squad name spans to prevent overflow.

Co-authored-by: dengjie5 <dengjie5@xiaomi.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 13:36:46 +08:00
Bohan Jiang
cb90249eac test(issues): match new metadata trigger label with count suffix (#3011)
The sidebar metadata trigger now reads "Metadata · N" (#3010), so the
exact-name button query stopped matching and 2 tests went red on main.
Relax the assertion to `/^Metadata\b/` — still anchors on the label but
tolerates the count suffix.
2026-05-21 18:25:36 +08:00
Bohan Jiang
cbd42dfcc4 feat(issues): align metadata trigger with sibling sections (#3010)
Reshape the sidebar metadata trigger so it visually matches the Pull
requests / Details / Parent issue headers (muted "Metadata · N" row
instead of an icon+label button). Clicking still opens the existing
JSON dialog — folding the bag inline pushed the rest of the sidebar
down too much when the payload was large.
2026-05-21 17:57:27 +08:00
Bohan Jiang
ab0228c2a1 feat(issues): collapse long metadata bags in sidebar MUL-2503 (#3003)
* feat(issues): collapse long metadata bags in sidebar (MUL-2503)

The metadata KV strip rendered every key inline, so issues with many
pinned keys pushed the rest of the sidebar far down. Keep the first
four rows visible and tuck the remainder behind a Show N more / Show
less toggle once the bag reaches five keys, mirroring the PR list
collapse rule.

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

* refactor(issues): hide metadata behind a JSON dialog (MUL-2503)

Metadata is an agent-facing free-form KV bag — the values almost never
mean anything to a human reader, and every property humans actually care
about already has a dedicated sidebar field (status, priority, assignee,
etc.). Rendering the first four keys inline still pushed real signal
down and added visual noise for no benefit, so drop the inline strip
entirely.

Replace the section with a small `{ }` Metadata button at the bottom of
the sidebar that opens a Dialog showing the formatted JSON. The button
hides itself when the bag is empty, so the common case stays completely
quiet. Removes the prior collapse threshold (and its `Show N more` /
`Show less` strings) since there is nothing to collapse anymore.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-21 17:18:57 +08:00
Bohan Jiang
0c767c0052 feat(issues): per-issue metadata KV (MUL-2017) (#2845)
* feat(issues): per-issue metadata KV (MUL-2017)

Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).

All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.

  GET    /api/issues/:id/metadata
  PUT    /api/issues/:id/metadata/:key   body: { "value": <primitive> }
  DELETE /api/issues/:id/metadata/:key

Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.

CLI: multica issue metadata {list,get,set,delete}
  multica issue list --metadata key=value (repeatable, AND)
  set has --type to override the default value-sniffing
Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)

- Fix two failing handler tests blocking backend CI:
  - reset decode target after delete so map merge does not mask removal
  - url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
  caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
  the issue has no keys so it stays quiet in the common case.

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

* feat(runtime): teach agents to read/write issue metadata (MUL-2017)

Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.

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

* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)

main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.

Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-21 16:35:45 +08:00
Naiyuan Qing
85e363370e Revert "feat(issues): Working filter + agent-working badge on board (MUL-2452…" (#2927)
This reverts commit dee5c7cf50.
2026-05-20 16:47:41 +08:00
Naiyuan Qing
dee5c7cf50 feat(issues): Working filter + agent-working badge on board (MUL-2452) (#2924)
* feat(issues): surface "agent working" on board + add Working filter (MUL-2452)

Adds a brand-color "agent working" badge to board cards / list rows so
users can see at a glance which issues have an active agent task, plus a
new "Working" toggle on the `/issues` and `/my-issues` headers (next to
the existing scope segmented control) that filters to those issues. The
toggle shows an avatar stack of the agents currently active on the
current surface + scope. Pure frontend: re-shapes the existing
workspace-wide `agentTaskSnapshot` cache via two new selectors
(`activeTasksByIssueOptions` / `workingIssueIdsOptions`), no new SQL,
endpoint, or DB field; WS `task:*` events already invalidate the
snapshot so the badge / filter update in realtime.

Project detail page keeps the per-card badge but intentionally omits the
header toggle (`showWorkingToggle={false}`) to leave the project
surface's filter dimensions unchanged.

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

* fix(issues): working filter column header reflects filtered count (MUL-2452)

Assignee-grouped board column headers kept showing the unfiltered cache
total when Working was on, because `PaginatedAssigneeBoardColumn` passed
`useLoadMoreByAssigneeGroup`'s cache-derived `total` straight to
`BoardColumn`. The hook still needs the cache total for hasMore, but the
displayed count must follow the visible-after-filter set.

Split the two: when Working is active the column header now uses
`group.totalCount` (set by applyWorkingFilterToGroups) for the assignee
path, and `issueIds.length` for the status path. Load-more keeps reading
from cache so paginated columns still see the full server total.

Regression tests cover applyWorkingFilterToGroups (total rewrite +
empty-group preservation), filterIssues workingOnly combinations, and an
end-to-end assertion via IssuesPage that proves the column header equals
the filtered count, not the cached value.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-20 16:35:58 +08:00
Bohan Jiang
b7082a01f1 fix(issues): retry button targets the row's agent (MUL-2457) (#2921)
* fix(issues): retry button targets the row's agent, not the assignee (MUL-2457)

The execution log retry button used to re-fire the issue's current
assignee instead of the agent that actually ran the clicked row. After
a reassignment, or for squad workers / @-mention agents, the rerun
landed on the wrong agent.

POST /api/issues/{id}/rerun now accepts an optional task_id: when set,
the rerun targets that task's agent (and reuses its leader/worker
role). An empty body keeps the assignee-driven CLI/API contract.

The execution-log retry button passes task.id, so per-row retry always
fires the correct agent. enqueueMentionTask gained a forceFreshSession
parameter so the new mention-path rerun keeps the same fresh-session
contract as the assignee path.

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

* fix(issues): inherit trigger provenance + fix cross-issue test (MUL-2457)

Address review feedback on PR #2921:

1. RerunIssue now inherits TriggerCommentID from the source task when
   sourceTaskID is valid. Without this, a per-row rerun of a comment-
   or mention-triggered task degrades into a generic issue run because
   the daemon's buildCommentPrompt path keys on TriggerCommentID. The
   inherited summary is rebuilt naturally inside the enqueue helpers
   (buildCommentTriggerSummary derives it from the comment ID).
2. The new cross-issue rejection test inserted a second issue without
   `number`, hitting uq_issue_workspace_number on a same-workspace
   collision with the fixture's issue. Both inserts now claim the next
   available per-workspace number (MAX(number)+1) — matching the
   pattern used by notification_listeners_test.

Added TestRerunIssueInheritsTriggerCommentFromSourceTask to lock the
trigger provenance contract.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-20 15:30:03 +08:00
Jiayuan Zhang
34988216ed feat(issues): show project segment in issue breadcrumb (MUL-2422)
* feat(issues): show project segment in issue breadcrumb (MUL-2422)

Render the issue's project (when present) between the workspace and any
parent-issue segment. Segment reflects the issue's own `project_id` so
the same URL produces the same breadcrumb from every entry point.

Failed/missing project queries fall back to an "Unknown project"
placeholder; loading shows a skeleton to avoid layout shift.

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

* fix(issues): cap project breadcrumb width to preserve title precedence

Constrain Project crumb to max-w-72 (matching ProjectChip) and add
min-w-0 to the title span so the flex compression order matches RFC
§5/§9: Project/Parent shrink before the current Issue title.

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

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 19:46:20 +02:00
Jiayuan Zhang
cd37b4e3d6 feat(settings): consolidate GitHub options under a dedicated Settings tab (MUL-2414) 2026-05-19 17:23:30 +02:00
Bohan Jiang
54368fd826 feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) (#2856)
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881)

Project Gantt now fetches its own scheduled-only data instead of riding the
Board/List pagination cache. The Unscheduled drawer and pagination warning
banner are gone, and any WS-driven issue change (create / update / delete)
invalidates the new cache so the timeline stays live.

- Backend: `GET /api/issues?scheduled=true` adds an
  `(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both
  ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler.
- Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single
  fetch and lives under its own cache key. WS handlers and mutations
  invalidate the prefix on create/update/delete so the bar reacts to
  start_date / due_date changes from other tabs and from this tab without
  waiting on the WS round-trip.
- GanttView: drops the Unscheduled section, the pagination warning banner,
  and the load-all button; renders only scheduled rows.
- Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`,
  `summarizeIssueListPagination`, and the gantt locale strings that
  supported the old plumbing.

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

* fix(projects): page through Gantt fetch and isolate per-view data sources

- Walk paginated `scheduled=true` issues until total is reached so projects
  with more than 500 scheduled bars no longer silently truncate.
- Gantt mode disables the bucketed Board/List query and reads its own
  scheduled cache for the project empty-state check, so the page never
  short-circuits Gantt with a Board-derived "no issues" CTA.
- `onIssueLabelsChanged` patches matching rows in the Project Gantt cache
  in-place, keeping label filters consistent after attach/detach from
  other tabs or agents.

MUL-1881

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 17:04:16 +08:00
Naiyuan Qing
39f43a9a98 refactor(editor): unify attachment rendering into a single <Attachment> component (#2850)
Collapse the five separate attachment render paths (file-card NodeView,
image NodeView, readonly markdown img/fileCard renderers, AttachmentList
standalone fallback, and the parallel packages/ui/markdown renderer) into
one <Attachment attachment={a} /> dispatcher.

Fixes a P0 visual regression: a PNG attached to a message but not inlined
in the markdown body used to render as a gray "file card" because
getPreviewKind() lacked an "image" branch and image rendering bypassed
the dispatcher entirely. Now every surface routes through <Attachment>,
so the same PNG renders as a real <img> with hover toolbar and
preview-modal everywhere.

Key changes:
- PreviewKind gains "image"; getPreviewKind() detects image/* + common
  extensions before the html/text branches (so svg stays image, not text).
- AttachmentPreviewModal gains case "image" (replaces the standalone
  ImageLightbox, which is deleted).
- New packages/views/editor/attachment.tsx owns all kind-aware routing
  (image | html | file) and dispatches preview modal + download via the
  existing useAttachmentPreview / useDownloadAttachment hooks. Subsumes
  the deleted AttachmentBlock.
- AttachmentInput.url accepts a forceKind hint so callers that *know*
  the structural kind (markdown ![](url), Tiptap image node) skip the
  filename-based autodetect — fixes a regression where empty or
  descriptive alt text would route an image to the file-card chrome.
- Tiptap NodeViews (file-card.tsx, image-view.tsx) shrink to thin
  wrappers that forward editor hints (selected, deleteNode, uploading)
  to <Attachment>.
- ReadonlyContent and AttachmentList each mount their own
  AttachmentDownloadProvider so url → record resolution works outside
  ContentEditor's provider.
- packages/ui/markdown gains optional renderImage / renderFileCard slot
  props; packages/views/common/markdown.tsx injects <Attachment> into
  those slots and threads message attachments through to chat /
  skill-file viewers.
- chat-message-list passes message.attachments to every <Markdown> call
  site and renders a standalone AttachmentList under each bubble for
  attachments not referenced in the body.

Tests: attachment.test.tsx covers 9 scenarios (record image / pdf / html;
url-only image with resolver hit and miss; uploading state; editable
delete; forceKind regression). attachment-preview-modal.test.tsx gains
image-dispatch cases. 652/652 unit tests pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:23:19 +08:00
Bohan Jiang
98ef021d1d feat(projects): add Project Gantt view (MUL-1881) (#2843)
* feat(projects): add Project Gantt view (MUL-1881)

Adds Gantt as a third option in the Project page's view toggle (Board /
List / Gantt). Bars span start_date → due_date; issues with only one
date render as markers, issues with neither are collapsed into an
Unscheduled section. Toolbar exposes day/week/month zoom and a
show-completed toggle. The Gantt view shares the existing IssuesHeader
filters/sort.

Implementation is self-rendered SVG/HTML — no new dependencies. UTC
day-aligned date math keeps bars on the right columns regardless of
viewer timezone.

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

* fix(projects): scope Gantt to project surface + warn on hidden pages

- IssuesHeader / IssueDisplayControls now take `allowGantt` (default false);
  only Project Detail opts in. /issues, /my-issues and the actor panel no
  longer expose a Gantt option that silently fell through to List, and the
  toggle icon falls back to List when a stored `viewMode === "gantt"` lands
  on a surface that doesn't render it.
- Project Gantt now surfaces a banner with hidden-issue count plus a
  Load-all action that drains every remaining paginated page into the
  cache via the new `useLoadAllRemaining` helper. Pagination summary comes
  from `myIssueListPaginationOptions`, which shares the existing cache key
  with `myIssueListOptions` so totals stay in sync with Board/List.
- ScheduledRow normalizes a `start_date > due_date` anomaly to min/max and
  outlines the bar with a destructive ring + tooltip note, instead of
  silently dropping the row.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 15:55:57 +08:00
Jiayuan Zhang
e0a6a39a47 feat(agents): list-only tasks panel with issue search (MUL-2391) (#2820)
* feat(agents): list-only tasks panel with issue search (MUL-2391)

Replace the agent detail tasks view-mode toggle with a fixed list view and
add a search bar that filters by issue title, identifier, or pinyin.

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

* fix(actor-issues): only show search empty state when searching

Previously the panel rendered the search empty state whenever the
filtered issue list was empty, which masked ListView's own status-based
empty states when status/priority/assignee/project/label filters
narrowed the list to 0. Now search_empty only renders when
`search.trim()` is non-empty and results are 0; otherwise ListView
takes over and shows its native empty states.

Refs MUL-2391

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-18 18:44:21 +02:00
Naiyuan Qing
baedc48f59 fix(editor): source-view highlight + HTML attachment open-in-new-tab (#2812)
* fix(editor): bump hast-util-to-html to v9 so lowlight output actually serializes

Source view of fenced ```html (and any other code block falling through to
the lowlight branch in ReadonlyContent) silently rendered as un-highlighted
escaped text. Root cause was a stale dep pin: `hast-util-to-html: ^4.0.1`
predates the package's ESM/named-export rewrite — v4 only exports a CJS
default function, so the `import { toHtml } from "hast-util-to-html"` in
code-block-static.tsx:19 and readonly-content.tsx:32 resolved to
`undefined` at runtime. The try/catch in both call sites caught the
"toHtml is not a function" throw and fell through to escapeHtml plain
text, so no `.hljs-*` spans ever made it to the DOM and the syntax-color
CSS added in #2808 had nothing to attach to.

Bumping to ^9.0.5 (matches the v9 line that lowlight@3 / remark / rehype
ship in the rest of the tree) makes the named `toHtml` export available
and source-view highlighting works.

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

* feat(editor): open HTML attachment in new tab + full-page preview route

Adds a third toolbar button to HtmlAttachmentPreview between Maximize and
Download: open the attachment in a new app tab (desktop) or browser tab
(web). The full-screen modal stays — they serve different scenarios:
modal for a quick "see it bigger" without leaving the issue context,
new-tab when the user wants to keep the rendered HTML around while
working on something else.

Components:
- New workspace path: `/{slug}/attachments/{id}/preview?name={filename}`.
  Lives outside the (dashboard) group on web so the iframe gets the full
  viewport — sidebar would defeat the point. Desktop registers the route
  inside `WorkspaceRouteLayout` so workspace context resolution still
  runs (no slug → no path is built).
- `packages/views/attachments/attachment-preview-page.tsx`: shared full-
  page view that reuses `useAttachmentHtmlText` for the iframe srcDoc.
  Sandbox stays `allow-scripts` (no allow-same-origin) — same security
  posture as the inline preview.
- `HtmlAttachmentPreview`: adds Open-in-new-tab button. Routes through
  `useNavigation().openInNewTab` when available (desktop), falls back to
  `window.open(getShareableUrl(path))` on web. Button is hidden when no
  workspace slug is in scope (shouldn't happen in practice, but the
  shared component must not throw outside a workspace route).

Tests cover: desktop openInNewTab call args, web window.open fallback,
and that the failure-mode toolbar still surfaces all three actions.

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

* fix(editor): drop now-stale @ts-expect-error on hast-util-to-html imports

v9 ships bundled type declarations, so the directives added for v4 trigger
TS2578 ("Unused '@ts-expect-error' directive") on CI typecheck.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:09:45 +08:00
Naiyuan Qing
5f1ced867c feat(editor): HTML attachments render like images (MUL-2345 v4) (#2798)
* feat(editor): HTML attachments render like images (MUL-2345 v4)

HTML attachments no longer wear the file-card chrome (icon + filename
row). They now render as a sandboxed iframe with a hover-revealed
right-top toolbar (Open / Download / Copy code), mirroring the image
attachment visual model.

- New HtmlAttachmentPreview owns the iframe + hover toolbar plus three
  states (loading / success / error). Failure mode keeps the toolbar
  pinned open and Open/Download enabled so the user is never stranded
  without an escape hatch — Copy code disables when the text body is
  unavailable.
- New AttachmentBlock thin dispatcher picks the renderer per kind:
  html + attachmentId + !uploading -> HtmlAttachmentPreview, else
  AttachmentCard. All three entry points (file-card NodeView, readonly
  file-card, standalone AttachmentList) call AttachmentBlock, so feature
  work on a new kind only touches one place.
- AttachmentCard collapses back to a pure file-card row UI: the inline
  HTML iframe branch (InlineHtmlIframe + inlineHtmlEnabled +
  showInlineHtml) is removed.
- AttachmentBlock added to the editor barrel export.

Sandbox/server-side defenses unchanged: sandbox="allow-scripts" (no
allow-same-origin), srcDoc, server still returns text/plain + nosniff
on the /content proxy.

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

* test(editor): pin three entry points to AttachmentBlock HTML route (MUL-2345)

Reviewer flagged that the v4 dispatcher refactor only had tests on the
shared AttachmentBlock + HtmlAttachmentPreview; the three real call
sites at file-card.tsx:59, readonly-content.tsx:279, and
comment-card.tsx:152 had no regression coverage. Reverting any one
would silently lose the inline HTML iframe path — the exact MUL-2330
regression we're meant to be locking down.

Each new test renders the real entry point with an HTML+attachmentId
fixture and asserts the dispatched iframe (sandbox=allow-scripts,
srcdoc) shows up while the AttachmentCard chrome (filename row) does
not. FileCardView and AttachmentList are exported from their files for
direct rendering, mirroring the existing CodeBlockView test pattern.

Mutation-tested locally: temporarily flipping each site back to
<AttachmentCard> turns its corresponding test red.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-18 17:44:32 +08:00