Compare commits

..

835 Commits

Author SHA1 Message Date
Jiayuan Zhang
509faab19f feat(cli): enhance version command with JSON output and build info
Add --output json flag, build date, Go version, and OS/arch to the
version command. Update Makefile and goreleaser to inject build date.
2026-04-12 02:12:35 +08:00
Jiayuan Zhang
29f7959db7 fix(cli): fix install script failing on repeated runs (#738)
The install script crashed silently on repeated `--local` runs due to
three issues:

1. `REPO_URL` includes `.git` suffix which returns 404 when used for
   GitHub releases API — `grep` found no match, exited 1, and
   `set -euo pipefail` killed the script with no error message.

2. `multica version` outputs "multica 0.1.26 (commit: ...)" but the
   version comparison used the full string, so it never matched the
   release tag and always attempted unnecessary upgrades.

3. Interrupted previous clones left a non-empty directory without
   `.git/`, causing `git clone` to fail on retry.
2026-04-12 01:53:39 +08:00
Jiayuan Zhang
bd1a7eb680 fix(cli): add upgrade logic to install script (#736)
When multica CLI is already installed, the install script now checks
for a newer version on GitHub Releases and upgrades automatically.
Homebrew installs use `brew upgrade`; binary installs re-download
the latest release. If already up to date, it skips.
2026-04-12 01:37:34 +08:00
Jiayuan Zhang
3198972d15 docs: add "Switching to Multica Cloud" section to self-hosting guides (#735)
Self-host users had no documented way to reconfigure their CLI for
multica.ai. Add a section after "Stopping Services" in both
SELF_HOSTING.md and self-hosting.mdx explaining the two options:
manual `config set` or re-running the install script without --local.
2026-04-12 01:35:50 +08:00
Jiayuan Zhang
d78be3b621 fix(cli): ensure cloud URLs are configured when not using local mode (#733)
After installing via `curl | bash` (default/cloud mode) or running
`multica setup` without a local server, the CLI config could retain
stale localhost URLs from a previous `multica config local` or
`--local` install. This caused `multica login` to connect to
localhost instead of multica.ai.

Fix: explicitly write cloud URLs (api.multica.ai / multica.ai) to
the config in both the install script's cloud mode and the setup
command's cloud fallback path.
2026-04-12 01:09:17 +08:00
Jiayuan Zhang
b0ee214154 feat: streamline self-hosting with one-click setup (#724)
* feat: streamline self-hosting experience with one-click setup

- Add `make selfhost` / `make selfhost-stop` for one-command Docker deployment
- Add `multica setup` CLI command (auto-detect local server, configure, login, start daemon)
- Add `multica config local` CLI command (configure for localhost defaults)
- Restructure SELF_HOSTING.md: simplified 4-step guide, moved advanced config to SELF_HOSTING_ADVANCED.md
- Add SELF_HOSTING_AI.md for AI agents to follow
- Document 888888 master verification code for non-production environments
- Document how to stop services
- Fix brew install typo: `multica-cli` → `multica` in SELF_HOSTING.md and self-hosting.mdx
- Update README.md and README.zh-CN.md with simplified self-host instructions
- Update CLI_AND_DAEMON.md with new setup/config local commands

* feat: add one-command installer script (curl | bash)

Add scripts/install.sh that handles the full setup in one command:

Self-host (default):
  curl -fsSL .../install.sh | bash
  → Checks Docker, clones repo, starts services, installs CLI, configures

Cloud (CLI only):
  curl -fsSL .../install.sh | bash -s -- --cloud
  → Installs CLI via Homebrew or binary download

Features:
- OS detection (macOS/Linux) with architecture support (amd64/arm64)
- Homebrew install with binary download fallback
- Idempotent: re-running updates existing installation
- Colored output with non-TTY fallback
- Docker availability check with helpful error messages

Updated docs (README, SELF_HOSTING, self-hosting.mdx, SELF_HOSTING_AI) to
show curl | bash as the primary install method.

* refactor: default install to cloud mode, add --local for self-host

- install.sh default is now cloud (CLI only, connects to multica.ai)
- Self-host uses --local flag: curl ... | bash -s -- --local
- Restructured README following Hermes Agent style:
  - Quick Install section front and center with curl | bash
  - CLI command reference table
  - Self-host as a callout under Quick Install
  - Removed redundant "Multica Cloud" / "CLI" sections
- Updated all docs (SELF_HOSTING, self-hosting.mdx, SELF_HOSTING_AI,
  README.zh-CN) to use --local flag for self-host curl command

* docs: remove redundant AI agent install snippet from README CLI section

* docs: add daemon stop command to README quick install sections

* feat: add --stop flag to install.sh for easy self-host shutdown

Users who installed via `curl ... | bash -s -- --local` can now stop
all services with `curl ... | bash -s -- --stop`. The stop command
shuts down Docker Compose services and the daemon.

Also updated SELF_HOSTING.md stopping section to show both methods.
2026-04-12 00:50:17 +08:00
Jiayuan Zhang
02c9480f44 fix(views): show agent live card immediately without waiting for messages (#727)
When navigating to an issue where an agent is already working, the
"Agent is working" card was delayed because it waited for both
getActiveTasksForIssue() AND listTaskMessages() to complete before
rendering. Now the card renders immediately after active tasks are
fetched, and messages load progressively in the background. Also
properly merges HTTP-loaded messages with any WebSocket-delivered
messages to avoid race conditions.
2026-04-12 00:21:39 +08:00
Jiayuan Zhang
3e4ae17596 fix(views): display comment attachments uploaded via CLI (#726)
commentToTimelineEntry() was dropping the attachments field, and
comment-card never rendered entry.attachments. Attachments uploaded
through the CLI (not embedded in markdown) were invisible in the UI.

- Add attachments to commentToTimelineEntry() conversion
- Add AttachmentList component that renders standalone attachments
  (skipping those already referenced in the markdown content)
- Render AttachmentList in both CommentRow and CommentCard
2026-04-12 00:11:25 +08:00
Jiayuan Zhang
c95ee27991 feat(views): support inline property editing on project list page (#725)
Allow users to modify project priority, status, and lead directly from
the project list without navigating to the detail page. Only the project
name/icon column navigates to the detail view now.
2026-04-12 00:10:53 +08:00
Bohan Jiang
f9f061de4c Merge pull request #717 from woosolkim/fix/docker-google-oauth-build-arg
fix(docker): pass NEXT_PUBLIC_GOOGLE_CLIENT_ID as build arg for self-hosting
2026-04-11 23:08:51 +08:00
Bohan Jiang
d11824807a fix(agent): handle braces in stderr log lines before openclaw JSON result (#718)
processOutput() used strings.Index(raw, "{") to find the JSON start,
but error lines like `raw_params={"command":"..."}` contain braces that
get matched first, causing JSON parsing to fail and the entire raw
stderr (including internal metadata) to be returned as the agent comment.

Now tries each '{' position until one successfully unmarshals as a valid
openclawResult, skipping braces embedded in log/error lines.
2026-04-11 23:07:58 +08:00
woosolkim
7c063a0e6f fix(docker): pass NEXT_PUBLIC_GOOGLE_CLIENT_ID as build arg for self-hosting
NEXT_PUBLIC_* env vars must be available at Next.js build time to be
inlined into the client bundle. Without this, the Google OAuth button
never renders in self-hosted Docker deployments even when the env var
is correctly set in .env.
2026-04-11 23:35:59 +09:00
Bohan Jiang
e477d64548 fix(cli): poll health endpoint instead of fixed sleep in daemon start (#716)
* fix(cli): poll health endpoint instead of fixed sleep in daemon start

The daemon start command waited a fixed 2 seconds then checked the
health endpoint once. If the daemon took longer to initialize (auth,
workspace loading), the check failed and printed a misleading error
even though the daemon started successfully.

Replace the single check with a polling loop (500ms interval, 15s
timeout) so the CLI waits for the daemon to actually be ready.

* fix(agent): rewrite openclaw tests to match new backend API

The openclaw backend was rewritten in #715 to parse a single JSON blob
instead of streaming NDJSON events. The tests still referenced the old
types (openclawEvent) and methods (handleOCTextEvent, etc.), causing a
build failure in CI.

Rewrite all tests to exercise the new processOutput method and
openclawInt64 helper.
2026-04-11 22:25:19 +08:00
Bohan Jiang
2e33084097 fix(agent): rewrite openclaw backend to match actual CLI interface (#715)
* fix(agent): use --message flag for OpenClaw CLI invocation

OpenClaw CLI changed its prompt flag from `-p` to `--message`. The old
flag caused tasks to fail immediately with "required option '-m,
--message <text>' not specified".

Fixes #713, relates to #703.

* fix(agent): rewrite openclaw backend to match actual CLI interface

- Replace unsupported flags (-p, --output-format, --yes) with correct
  ones (--message, --json, --local, --session-id)
- Read JSON result from stderr (where openclaw writes it)
- Parse openclaw's actual output format ({payloads, meta})
- Auto-generate session ID for each task execution
- Show "live log not available" hint in agent live card when timeline
  is empty (openclaw doesn't support streaming)
2026-04-11 22:14:47 +08:00
Jiayuan Zhang
b3f98ef95d fix(server): skip auto-comment when agent already posted during task (#712)
* fix(server): skip auto-comment when agent already posted during task

In CompleteTask(), check if the agent already posted a comment on the
issue since the task started. If so, skip the automatic output comment
to avoid duplicates. This preserves the fallback for agents that don't
post comments via CLI.

Closes MUL-609

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

* fix(server): use StartedAt instead of CreatedAt for duplicate check

CreatedAt is the enqueue time, not execution start. If a previous task
posted a comment between enqueue and start of the next task, it would
incorrectly suppress the auto-comment for the later task.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:27:02 +08:00
Jiayuan Zhang
ff241af8d7 fix(views): trim search input in assignee and filter pickers (#709)
Leading spaces in search queries caused `.includes()` to fail because
names don't contain leading whitespace. Apply `.trim()` before
`.toLowerCase()` in assignee-picker, actor filter, and project filter.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:06:06 +08:00
pradeep7127
d9be9465c3 fix(storage): support custom S3 endpoints for self-hosted deployments (MinIO) (#681)
* fix(storage): support custom S3 endpoints for self-hosted deployments

When AWS_ENDPOINT_URL is set, the S3 client now uses path-style
addressing and routes requests to the custom endpoint (e.g. MinIO).
Returns path-style URLs (endpoint/bucket/key) instead of virtual-hosted
URLs so attachments are accessible on local setups.

Also falls back to STANDARD storage class for custom endpoints since
MinIO and other S3-compatible stores do not support INTELLIGENT_TIERING.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(storage): handle custom endpoint URLs in KeyFromURL

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 20:35:31 +08:00
Bohan Jiang
5def4b62e0 fix(web): upgrade Next.js to ^16.2.3 for CVE-2026-23869 (#706)
High-severity DoS vulnerability (CVSS 7.5) in App Router — specially
crafted requests to RSC endpoints cause excessive CPU consumption.
Patched in Next.js 16.2.3.

Ref: https://github.com/multica-ai/multica/issues/701
2026-04-11 18:23:13 +08:00
Bohan Jiang
c72df9b127 Merge pull request #699 from multica-ai/agent/j/696a5ce1
docs: add v0.1.23 and v0.1.24 changelog (2026-04-11)
2026-04-11 15:34:46 +08:00
Jiang Bohan
1de88a9412 docs: add v0.1.23 and v0.1.24 changelog entries (2026-04-11) 2026-04-11 15:33:59 +08:00
Bohan Jiang
3cd26c1d82 Merge pull request #672 from pasmud/fix/selfhost-docker-build
Thanks for the thorough fix! 🎉
2026-04-11 14:58:42 +08:00
zerone0x
cc9a8ad6ec fix(daemon): make meta-skill workflow defer to agent Skills instead of hardcoding (#675)
Replaces the hardcoded assignment-triggered workflow in buildMetaSkillContent()
with a minimal version that defers to agent Skills and Identity. Keeps platform
capability docs and status management steps intact.

Fixes #669

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 14:48:58 +08:00
jayavibhavnk
41d4ac3877 fix(server): add missing WorkspaceID to agent comment creation (#688)
createAgentComment omitted WorkspaceID when calling CreateComment,
causing all agent comments (progress updates, completion messages) to
silently fail against the NOT NULL constraint on comment.workspace_id.
The issue variable is already fetched on the preceding line for mention
expansion, so this adds the missing field to match the handler path in
comment.go.
2026-04-11 14:38:40 +08:00
Zheng Li
a76194744a feat(cli): add --project filter to issue list (#691)
Co-authored-by: nocoo <nocoo@users.noreply.github.com>
2026-04-11 14:37:24 +08:00
Bohan Jiang
34695ad78b Merge pull request #692 from jwcastillo/fix/docker-web-chown-nextjs
fix(docker): chown runtime files to nextjs user in web image
2026-04-11 14:35:51 +08:00
Jiayuan Zhang
7008d03b02 feat: notify parent issue subscribers on sub-issue changes (#685)
* feat(notifications): notify parent issue subscribers on sub-issue changes

When a sub-issue receives a change (status, assignee, priority, comment, etc.),
parent issue subscribers are now also notified. Deduplicates against direct
subscribers to avoid double notifications. The inbox item still points to the
sub-issue so clicking the notification navigates to the actual change.

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

* fix(notifications): parent subscriber inbox items now point to sub-issue

Split notifyIssueSubscribers into subscriberIssueID (which issue's
subscribers to query) and targetIssueID (which issue the inbox item
links to). When notifying parent subscribers, the inbox item correctly
points to the sub-issue where the change occurred, so clicking the
notification navigates to the right place.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:33:00 +08:00
Bohan Jiang
5956280d56 fix(server): don't inherit parent agent mentions when reply has its own mentions (#693)
When a reply explicitly @mentions anyone (agents or members), the user
is making a deliberate choice about who to involve. Previously, replying
with @AgentB under a comment mentioning @AgentA would trigger both agents.
Now parent mentions are only inherited when the reply has no mentions at all.
2026-04-11 14:29:01 +08:00
Wen
21fea91d23 fix(docker): chown runtime files to nextjs user in web image
public/ is mode 750 locally, so COPY into the runner stage landed files as
root and the nextjs user fell under other perms, causing EACCES on scandir
at startup. Add --chown=nextjs:nodejs to the standalone/static/public COPYs.
2026-04-11 01:29:45 -04:00
Jiayuan Zhang
82bbce98fd fix(security): add workspace ownership checks to daemon API routes (#684)
* fix(security): add workspace ownership checks to all daemon API routes

Switch daemon routes from middleware.Auth to middleware.DaemonAuth and
add per-handler workspace ownership verification. This prevents
cross-workspace access to runtimes, tasks, usage, and daemon lifecycle
endpoints (HIGH-1/2/3 + CHAIN-1/2/3).

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

* fix(security): support mdt_ daemon tokens in DaemonRegister + add regression tests

DaemonRegister now handles both auth paths:
- mdt_ daemon tokens: verify workspace match, skip member check, zero OwnerID
  (SQL COALESCE preserves existing owner on upsert)
- PAT/JWT: existing member check + OwnerID from member

Also adds WithDaemonContext helper and regression tests covering:
- Successful register with daemon token
- Workspace mismatch rejection
- Cross-workspace heartbeat rejection
- Cross-workspace task status rejection

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:49:23 +08:00
Jiayuan Zhang
f4016fc721 fix(server): validate workspace ownership for attachment uploads and queries (#683)
Prevent cross-workspace attachment injection (CRIT-3) by verifying
issue_id/comment_id belong to the caller's workspace before creating
attachment records. Add workspace_id filter to ListAttachmentsByCommentIDs
query (MED-3) to prevent cross-workspace attachment data leakage.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 04:33:24 +08:00
Jiayuan Zhang
6c5879215d fix: sanitize markdown rendering in comments and shared renderers (#679)
* fix: sanitize markdown rendering in comments and shared renderers

Add rehype-sanitize to both ReadonlyContent and Markdown components so
that raw HTML parsed by rehype-raw is sanitized against a strict
allowlist before reaching the DOM. On the backend, add a bluemonday
sanitization pass when creating and updating comments to strip
dangerous tags as defense-in-depth.

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

* fix: add mention:// protocol to sanitize allowlist and validate file card URLs

- Add mention:// to rehype-sanitize protocols.href in both ReadonlyContent
  and Markdown so @mention links survive sanitization
- Validate data-href on file cards to only allow http(s) URLs, blocking
  javascript: and data: schemes in both frontend click handler and backend
  bluemonday policy
- Narrow class attribute allowlist to specific elements (code, div, span, pre)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:44:30 +08:00
Jiayuan Zhang
2610d2dc3f chore: remove .pid files from repo and gitignore them (#680)
These are runtime artifacts created by Conductor for worktree process
management. They should never be tracked in git.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 03:37:20 +08:00
Jiayuan Zhang
faee939312 feat(issues): add project filter to Issues tab (#671)
Support filtering issues by project in the Issues tab filter dropdown,
including a "No project" option for issues without a project assigned.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:23:45 +08:00
pasmud
ea15f94341 fix(docker): fix self-hosting Docker build failures
The self-hosting Docker Compose setup fails to build on a clean clone due to several issues:

1. Dockerfile.web did not copy .npmrc into the deps stage. The project uses shamefully-hoist=true, so without it pnpm produces a different node_modules layout and module resolution breaks.

2. The builder stage copied individual node_modules directories from the deps stage (COPY --from=deps). This breaks pnpm's symlink structure -- especially on Windows where symlinks resolve to host paths. Additionally, packages/tsconfig has zero dependencies so its node_modules never exists, causing a hard COPY failure. Fixed by copying the full workspace from deps and running an offline pnpm install to re-link after source overlay.

3. next.config.ts imports dotenv but it was not declared as a direct dependency in apps/web/package.json. It resolves locally as a hoisted transitive dep but fails the TypeScript type check during next build in Docker.

4. docker/entrypoint.sh gets CRLF line endings on Windows due to git autocrlf, which breaks the shebang (container looks for /bin/sh\r). Added .gitattributes to enforce LF for shell scripts and a sed strip in the Dockerfile as a safety net.
2026-04-11 00:33:18 +10:00
Jiayuan Zhang
762bc92b2d fix(landing): replace "AI-Native Task Management" with landing page messaging (#670)
Use "Project Management for Human + Agent Teams" across all page titles,
OpenGraph metadata, and structured data to align with the actual landing
page hero and footer content.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:36:45 +08:00
Jiayuan Zhang
8db9099207 feat(search): add page navigation to cmd+k command palette (#665)
* feat(search): add page navigation to cmd+k command palette

Users can now search and navigate to sidebar pages (Inbox, My Issues,
Issues, Projects, Agents, Runtimes, Skills, Settings) directly from
the cmd+k dialog. Pages are shown in a dedicated "Pages" group and
filtered by query with keyword matching.

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

* fix(search): only show pages when query is entered

Pages section was pushing down the Recent Issues list when the dialog
first opens. Now pages only appear when the user types a matching query.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:15:16 +08:00
Jiayuan Zhang
904192b45c fix(web): correct project kanban issue counts (#667) 2026-04-10 21:13:25 +08:00
Jiayuan Zhang
0cceeee690 feat(projects): replace overview tab with sidebar properties panel (#662)
Removes the Overview/Issues tab system — clicking a project now shows
issues directly. Project properties (icon, title, status, priority,
lead, progress, description) are moved to a collapsible right sidebar,
matching the issue detail layout pattern.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:02:09 +08:00
Jiayuan Zhang
f1d81cdfaa feat(search): add project search support to Cmd+K search (#663)
Projects are now searchable alongside issues in the Cmd+K search dialog.
Results are grouped by type (Projects / Issues) with project icon, status,
and description snippet highlighting.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:59:32 +08:00
Jiayuan Zhang
2d4b959407 fix(docker): remove COPY for non-existent tsconfig/node_modules (#661)
* fix(docker): remove COPY for non-existent tsconfig/node_modules

The @multica/tsconfig package has zero dependencies, so pnpm install
never creates a node_modules directory for it. The COPY --from=deps
instruction fails with "not found" during docker compose build.

Closes #658

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

* fix(docker): add dotenv as explicit dependency for web app

next.config.ts imports dotenv to load .env for REMOTE_API_URL, but
dotenv was never declared as a dependency. It worked locally as a
hoisted transitive dep but fails in Docker's stricter module resolution.

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

* docs: fix daemon setup instructions for local Docker deployments

The daemon setup section in SELF_HOSTING.md had production URLs as the
active example and local Docker URLs commented out. Since this is a
self-hosting guide, local Docker should be the primary example.

Key changes:
- Make local Docker URLs the default in daemon setup examples
- Add explicit warning that CLI defaults to hosted service
- Add 'multica config set' instructions for persistent setup
- Add link from Quick Start to daemon setup section
- Clarify that daemon runs on host machine, not inside Docker
- Update CLI_AND_DAEMON.md self-hosted section similarly

Closes #660

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:58:32 +08:00
Jiayuan Zhang
54d452e20d feat(search): show recent issues in cmd+k dialog (#656)
* feat(search): show recent issues list when cmd+k opens

When opening the cmd+k search dialog, display a list of recently visited
issues instead of the empty placeholder. Visits are tracked via a
workspace-scoped persisted Zustand store (max 20 items).

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

* fix(search): close cmd+k dialog on single ESC press

cmdk was consuming the first ESC to clear internal state, requiring a
second press to close the dialog. Intercept ESC on the CommandPrimitive
and close the dialog directly.

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

* fix(search): move ESC handler to input to prevent double-ESC

The previous handler on CommandPrimitive didn't fire because cmdk
intercepts ESC at the input level. Moving the onKeyDown to
CommandPrimitive.Input ensures it fires before cmdk processes it.

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

* fix(search): use capture-phase ESC listener to close dialog reliably

The previous onKeyDown approach on the Input didn't work because
base-ui Dialog's internal focus management handled ESC before the
React synthetic event. Use a document-level capture-phase listener
that fires before all other handlers and stops propagation.

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

* test(search): cover single-escape command palette close

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:48:43 +08:00
Jiayuan Zhang
9b62485a86 feat: add pin to sidebar for issues and projects (#653)
* feat: add pin to sidebar for issues and projects

Add per-user pinning of issues and projects to the sidebar for quick access.

- New `pinned_item` table with per-user, per-workspace scoping
- REST API: GET/POST /api/pins, DELETE /api/pins/{type}/{id}, PUT /api/pins/reorder
- Sidebar "Pinned" section between Personal and Workspace nav (hidden when empty)
- Pin/unpin actions in issue and project detail dropdown menus
- Optimistic mutations with WebSocket invalidation for real-time sync

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

* feat: add drag-and-drop reordering and visible pin buttons

- Sidebar pinned items now support drag-and-drop reordering via @dnd-kit
- Add visible pin/unpin icon button in issue and project detail headers
- Add useReorderPins mutation with optimistic updates

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

* fix: remove drag handle and fix page refresh after reorder

- Remove GripVertical drag handle — whole item is now draggable, aligning
  with other sidebar elements
- Prevent link navigation after drag using wasDragged ref
- Remove onSettled invalidation from reorder mutation to prevent
  unnecessary refetch after optimistic update

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:00:25 +08:00
Jiayuan Zhang
cce210ed3a feat(assign): sort members & agents by user's assignment frequency (#652)
The Assign dropdown now sorts members and agents by how frequently the
current user assigns issues to them. Frequency is computed from two
sources: assignee_changed activities in the activity log and initial
assignments on issues created by the user.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:45:08 +08:00
Jiayuan Zhang
356ff002dd feat(projects): show completion progress in project list (#651)
* feat(projects): show completion progress (done/total issues) in project list

Add a progress column to the projects list page that displays a mini progress
bar and done/total issue count for each project. Backend batch-fetches issue
stats per project using a single query for efficiency.

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

* feat(projects): show progress on project overview page

Add a progress bar with done/total (percentage) to the project detail
overview tab, computed from the already-loaded project issues.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:36:49 +08:00
Jiayuan Zhang
c234359857 feat(views): auto-fill project when creating issue via C shortcut on project page (#650)
When pressing "C" to create a new issue from a project detail page,
automatically set the project_id so the issue is linked to the current project.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:26:55 +08:00
Bohan Jiang
8bcb773304 fix(desktop): disable web security for CORS and fix dev server port (#648)
- Set webSecurity: false in BrowserWindow to bypass CORS when
  connecting to remote API (standard Electron practice)
- Fix renderer dev server to port 5173 so localStorage persists
  across restarts (prevents losing login state)
2026-04-10 18:20:30 +08:00
LinYushen
b52c048c8e fix(my-issues): use server-side filtering instead of client-side (#649)
* fix(my-issues): use server-side filtering instead of client-side

My Issues was fetching ALL workspace issues and filtering client-side,
causing the Done column to show wrong counts (269 vs user's actual
count) and only 2-3 done issues to appear from the first 50-item page.

Backend:
- Add creator_id and assignee_ids (uuid[]) filters to ListIssues,
  ListOpenIssues, and CountIssues SQL queries
- Parse creator_id and assignee_ids (comma-separated) query params

Frontend:
- Add myIssueListOptions with per-scope server-filtered queries
- Each tab now calls the API with the right filter:
  Assigned → assignee_id, Created → creator_id,
  My Agents → assignee_ids
- Add useLoadMoreMyDoneIssues for server-filtered done pagination
- WS events invalidate My Issues cache via issueKeys.myAll

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

* refactor(my-issues): merge duplicate load-more hooks into one

Both board-view and list-view were unconditionally calling two hooks
(useLoadMoreDoneIssues + useLoadMoreMyDoneIssues) and picking one at
runtime. Merged into a single useLoadMoreDoneIssues with an optional
myIssues param so only one hook runs per render.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:54:13 +08:00
LinYushen
f53cdf3157 fix(views): show user-scoped done count on My Issues page (#647)
The Done column on My Issues was displaying the workspace-wide total
(e.g. 269) instead of the current user's done issue count, because
BoardView/ListView read doneTotal directly from the shared cache.

Add an optional doneTotal prop to BoardView and ListView so the parent
can override the displayed count. MyIssuesPage now computes the count
from the client-filtered issue list and passes it through.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:00:18 +08:00
Bohan Jiang
8056c49909 docs: add v0.1.22 changelog with categorized sections
* docs: add v0.1.22 changelog (2026-04-10)

* docs: rewrite v0.1.22 changelog with categorized sections

- Add features/improvements/fixes categories to changelog type and component
- Remove desktop/Electron mentions (not yet released)
- Rewrite all entries with detailed descriptions based on actual commit messages
- Component renders category headers when present, falls back to flat list for older entries
- Both en and zh updated

* docs: trim v0.1.22 changelog entries for conciseness
2026-04-10 16:54:28 +08:00
Naiyuan Qing
d0edf2e4d5 Merge pull request #645 from multica-ai/feat/desktop-drag-reorder-tabs
feat(desktop): drag-to-reorder tabs via dnd-kit
2026-04-10 16:43:40 +08:00
Naiyuan Qing
6793f041ce Merge pull request #643 from multica-ai/agent/agent/d7add9d3
fix(desktop): add Geist font loading for consistent typography
2026-04-10 16:39:26 +08:00
Naiyuan Qing
b743db35af feat(desktop): drag-to-reorder tabs via dnd-kit
Adds horizontal drag-and-drop reordering for the desktop tab bar using
@dnd-kit/sortable, with axis + parent constraints so tabs only slide
horizontally within the bar. Order is persisted automatically through
the existing tab-store partialize.

Also brings tab-store into the standardized storage pipeline introduced
in 85cff154 — it was the last persist store still using vanilla zustand
persist instead of createPersistStorage(defaultStorage). Storage key
multica_tabs is unchanged so existing user data is preserved.

- apps/desktop: add @dnd-kit/{core,sortable,modifiers,utilities}
- tab-store: moveTab(from, to) action via arrayMove (preserves router refs)
- tab-store: persist storage → createJSONStorage(createPersistStorage(defaultStorage))
- tab-bar: DndContext + SortableContext(horizontalListSortingStrategy)
- tab-bar: restrictToHorizontalAxis + restrictToParentElement modifiers
- tab-bar: PointerSensor distance:5 to disambiguate click vs drag
- tab-bar: stopPropagation on close-button pointerdown to avoid drag start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:38:27 +08:00
Naiyuan Qing
a3149858f5 fix(desktop): add Geist font loading for consistent typography
Desktop app was missing Geist font — the CSS variable `--font-sans` referenced
by `@theme inline` in tokens.css was never defined, causing fallback to the
Chromium default system font. Web app worked because Next.js `next/font/google`
injected the variable.

Fix: add @fontsource/geist-sans and @fontsource/geist-mono, import the font
CSS in main.tsx, and define --font-sans/--font-mono in globals.css.

Closes MUL-504

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:35:45 +08:00
Jiayuan Zhang
0f86611c41 fix: support multiline display for Master Agent input (#638)
* fix(views): support multiline display for agent text content

- TextRow in agent-live-card: show collapsible multiline content instead
  of only the last line
- Chat user message bubble: add whitespace-pre-wrap to preserve line breaks

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

* revert: remove out-of-scope TextRow change in agent-live-card

Only the chat bubble multiline fix is needed for this issue.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:28:31 +08:00
Jiayuan Zhang
17ae320dd2 feat(docs): add documentation site with Fumadocs (#634)
Set up a documentation site at apps/docs using Fumadocs (Next.js App Router).
Migrated existing docs (README, SELF_HOSTING, CLI_AND_DAEMON, CLI_INSTALL,
CONTRIBUTING, AGENTS) into structured MDX content with sidebar navigation
and full-text search.

Content structure:
- Getting Started: Cloud quickstart, self-hosting guide
- CLI & Daemon: Installation, full command reference
- Guides: Quickstart, agents overview
- Developers: Contributing guide, architecture docs

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:28:23 +08:00
Bohan Jiang
6b8afb1d3d fix(views): use fake timers globally in login-page tests to prevent input-otp timer leak (#642)
input-otp sets internal timers that fire after jsdom tears down window,
causing "ReferenceError: window is not defined" unhandled errors in CI.
Using fake timers suite-wide ensures no real timers escape after cleanup.
2026-04-10 16:23:42 +08:00
Jiayuan Zhang
bf8abba24d fix(db): relax pending task unique index to per-(issue, agent) (#637)
The idx_one_pending_task_per_issue index only allowed one pending task
per issue across all agents, causing different agents' queued/dispatched
tasks to block each other. This mismatched the code-level dedup which
checks per (issue_id, agent_id). Replace with idx_one_pending_task_per_issue_agent
on (issue_id, agent_id) so each agent can independently have one pending task.

Fixes MUL-495

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:21:20 +08:00
LinYushen
63ca8d7d89 fix(views): improve daily token usage chart readability (#641)
* fix(views): improve daily token usage chart readability

- Fix Y-axis showing scrambled/truncated tick labels by computing
  explicit nice ticks and using compact number formatting (100M not 100.0M)
- Simplify token categories from 4 (Input/Output/Cache Read/Cache Write)
  to 3 (Input/Output/Cached) — cache write merged into input
- Replace noisy stacked area chart with clean single-area total trend,
  with a custom tooltip showing per-category breakdown and total
- Increase Y-axis width to prevent label clipping

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

* fix(views): handle floating point edge case in formatTokens

Use modulo + threshold instead of Number.isInteger to avoid floating
point precision issues (e.g. 2.5M * 4 = 10.000000000000004).

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

* fix(views): keep 4 token categories consistent between chart tooltip and summary cards

Revert the 3-category simplification (Cached/Input/Output) back to the
original 4 categories (Input/Output/Cache Read/Cache Write) so the chart
tooltip matches the summary cards on the same page.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:20:03 +08:00
Bohan Jiang
28b9bf85ee feat(daemon): add minimum Claude Code version check (#625)
* feat(daemon): add minimum Claude Code version check during runtime registration

The daemon now validates the detected agent CLI version against a
minimum requirement before registering a runtime. Claude Code requires
>= 2.0.0 (when --output-format stream-json and --permission-mode
bypassPermissions were introduced). Older versions are skipped with a
warning log, preventing silent failures.

Closes #569

* feat(daemon): add minimum Codex CLI version check (>= 0.100.0)

The `codex app-server --listen stdio://` flag was introduced in v0.100.0.
Older versions lack this flag and fail silently. Add codex to the
MinVersions map so the daemon skips outdated codex CLIs with a clear
warning, matching the existing Claude version check.

Refs #490
2026-04-10 16:09:56 +08:00
Naiyuan Qing
de88219edc Merge pull request #640 from multica-ai/fix/drag-drop-overlay
feat(core): storage standardization + workspace isolation
2026-04-10 16:06:06 +08:00
Naiyuan Qing
1e0d2b8606 fix(auth): logout clears workspace_id and query cache
Previously logout only removed multica_token, leaving workspace_id
and TanStack Query cache intact — a security issue on shared devices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:00:04 +08:00
Naiyuan Qing
85cff15427 feat(core): standardize storage + workspace isolation for persist stores
Unify all client-side persistence through StorageAdapter and add
workspace-scoped key namespacing (${key}:${wsId}).

- createPersistStorage: bridge for Zustand persist → StorageAdapter DI
- createWorkspaceAwareStorage: dynamic namespace by current workspace
- Migrate 6 persist stores (navigation, draft, view, scope, my-issues-view, chat)
- Rehydration registry: stores auto-rehydrate on workspace switch
- clearWorkspaceStorage: cleanup on workspace delete / member removal
- Chat store: namespace keys + rehydrate on workspace switch
- Factory view stores (createIssueViewStore): auto-register for rehydration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:59:53 +08:00
Naiyuan Qing
ee46fd6064 fix(editor): address review — complete migration, fix imports, clean dead code
- Add showDropOverlay={false} to projects/ ContentEditors (no upload support)
- Use barrel exports from ../../editor instead of direct file imports
- Remove ring-brand/30 from CommentInput for visual consistency
- Remove dead internal overlay code from ContentEditor (dragOver state,
  drag handlers, overlay JSX, document listeners, showDropOverlay prop)
- Remove unused .editor-drop-overlay CSS
- Update issue-detail test mock with useFileDropZone/FileDropOverlay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:49:40 +08:00
Jiayuan Zhang
b439cfe9ea feat: add 'C' keyboard shortcut for New Issue (#635)
* feat(views): add "C" keyboard shortcut to open new issue modal

Adds a global keyboard shortcut matching Linear's convention — pressing
"C" when not focused on an input/editor opens the create-issue modal.
Also displays the shortcut hint in the sidebar button.

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

* fix(views): match "C" shortcut badge style to search ⌘K badge

Use the same kbd styling (rounded border, bg-muted, font-mono) as the
search trigger so the two shortcut hints look consistent.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:48:18 +08:00
Jiayuan Zhang
17ad3b2f3b Merge pull request #618 from multica-ai/agent/emacs/2023d753
feat(dx): simplify local dev and self-hosting setup
2026-04-10 15:46:27 +08:00
Bohan Jiang
ee3c849c52 fix(skills): detect GitHub default branch instead of hardcoding "main" for skills.sh imports (#632)
Repos hosted on GitHub can use any branch name as default (main, master, etc.).
The skills.sh import was hardcoding "main" in raw.githubusercontent.com URLs,
causing 404s when fetching SKILL.md from repos with a different default branch.

Now queries the GitHub API (/repos/{owner}/{repo}) to get the actual default
branch before fetching files.

Fixes #517
2026-04-10 15:44:18 +08:00
Bohan Jiang
5f888c75c4 feat(views): mobile-responsive layout for sidebar and inbox (#630)
* fix(layout): add mobile sidebar trigger for small screens

The sidebar already renders as a Sheet (drawer) on mobile via the
existing shadcn sidebar component, but there was no trigger button
for users to open it. This adds a mobile-only (md:hidden) header
bar with a SidebarTrigger in the DashboardLayout so users on phones
can access the sidebar navigation.

Closes #593

* feat(views): add mobile-responsive layout for inbox page

On mobile (<768px), switch from resizable two-panel layout to a
full-screen list/detail toggle. Tapping a notification shows the
detail view full-screen with a back button; the sidebar trigger
from the dashboard layout remains accessible.
2026-04-10 15:41:57 +08:00
LinYushen
a25886102a feat(agent): add Hermes Agent Provider via ACP protocol (#623)
* feat(agent): add Hermes Agent Provider via ACP protocol

Integrate Hermes as a new agent backend using the ACP (Agent
Communication Protocol) JSON-RPC 2.0 over stdio — the same pattern
as the Codex provider but with ACP-specific methods.

- New hermesBackend spawns `hermes acp` and drives initialize →
  session/new → session/prompt lifecycle
- Handles session/update notifications: agent_message_chunk,
  agent_thought_chunk, tool_call, tool_call_update, usage_update
- Auto-approves tool executions via HERMES_YOLO_MODE env var
- Supports session resume, model override, system prompt injection
- Token usage extracted from PromptResponse and usage_update events
- Auto-detected at daemon startup via MULTICA_HERMES_PATH env var

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

* feat(ui): optimize runtime icons and fix create-agent dialog overflow

- Replace OpenClaw pixel-art icon (32 rects) with clean vector paths
- Add Hermes provider icon (NousResearch mascot, 48x48 webp data URI)
- Use provider-specific icons in runtime selector instead of generic Monitor
- Fix dialog overflow: add min-w-0 to grid item so truncate works

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

* fix(agent): add required mcpServers param to Hermes ACP session/new

ACP SDK v0.11.2 requires mcpServers as a mandatory field in
NewSessionRequest. Without it, Pydantic validation fails with
"Invalid params" and the agent immediately errors out.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:40:11 +08:00
Jiayuan Zhang
2c1d1d989c fix(daemon): symlink Codex sessions dir to shared home for discoverability (#627)
Per-task CODEX_HOME isolated session logs in per-task directories, making
them invisible from the global ~/.codex/sessions/ where users expect to
find them. Symlink the sessions directory back to the shared home so
Codex writes session logs to the global location while keeping skills
isolated per task.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:38:34 +08:00
Naiyuan Qing
4268b7891a feat(core): add workspace-aware storage for scoped persist stores
Create createWorkspaceAwareStorage that dynamically namespaces
localStorage keys by workspace ID (e.g. "multica_issue_draft:ws_abc").
Wire setCurrentWorkspaceId into workspace store lifecycle methods and
migrate all workspace-scoped stores (draft, view, scope) to use it.
Navigation store intentionally left user-scoped without namespace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:27:18 +08:00
Naiyuan Qing
cc672b8009 feat(core): add createPersistStorage utility for Zustand persist middleware
Bridge between Zustand persist middleware's StateStorage and the existing
StorageAdapter DI system, with optional workspace-scoped key namespacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:20:11 +08:00
Naiyuan Qing
66cb5d924a fix(editor): lift drag-drop overlay to outer container for better UX
When editors are empty, the internal drop overlay was too small to be
useful. Move the overlay to the parent container with a lighter style
so the drop target covers the full input area regardless of content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:18:38 +08:00
Jiayuan Zhang
c7e5aedb14 fix(server): add startup warnings for missing JWT_SECRET and RESEND_API_KEY
When these env vars are not configured, the server now prints clear
warning messages at startup so users know what to fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:17:53 +08:00
Bohan Jiang
66dec60f71 fix(core): invalidate parent children cache when sub-issues are deleted (#633)
useDeleteIssue and useBatchDeleteIssues only invalidated the main issues
list after deletion, leaving the parent issue's children cache stale.
This caused deleted sub-issues to remain visible in the parent issue view
until a full page refresh. Now both mutations look up the deleted issue's
parent_issue_id and invalidate the corresponding children query on settle,
matching the pattern already used in the WebSocket handler.
2026-04-10 15:15:03 +08:00
Jiayuan Zhang
ec71a41d8f feat(deploy): add full-stack Docker Compose for self-hosting
Add a one-command self-hosting setup: `docker compose -f docker-compose.selfhost.yml up -d`
starts PostgreSQL, backend (with auto-migration), and frontend.

Changes:
- docker-compose.selfhost.yml: full stack orchestration (postgres + backend + frontend)
- Dockerfile: add entrypoint.sh that auto-runs migrations before server start
- Dockerfile.web: multi-stage Next.js build with standalone output
- docker/entrypoint.sh: migration + server startup script
- .dockerignore: exclude unnecessary files from Docker builds
- apps/web/next.config.ts: conditional standalone output for Docker builds
- SELF_HOSTING.md: rewrite with Docker Compose as primary approach
- README.md: update self-host section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:11:18 +08:00
Bohan Jiang
ca7ba48934 fix(agents): invalidate runtimes cache on daemon events (#624)
The Agents page never received runtime cache updates when daemons
registered or deregistered, causing the Create Agent dialog to show
"No runtime available" even when runtimes existed. This happened because
daemon events were only handled by the Runtimes page component, not
globally.

- Add daemon:register to the centralized realtime sync refresh map
- Skip daemon:heartbeat in the generic handler to avoid excessive refetches
- Invalidate runtimes on WS reconnect alongside other workspace data
- Show a loading indicator in the Create Agent dialog while runtimes load
2026-04-10 14:50:34 +08:00
Yevanchen
63895343e3 Fix Claude stream-json startup hangs (#592) 2026-04-10 14:42:28 +08:00
Bohan Jiang
88982ad23f feat(issues): display token usage per issue in detail sidebar (#581)
* feat(issues): display token usage per issue in detail sidebar

Add a new "Token usage" section to the issue detail right sidebar that
shows aggregated input/output tokens, cache tokens, and run count across
all tasks for the issue. Backed by a new SQL query and API endpoint.

* fix(db): add index on agent_task_queue(issue_id) for usage queries

The GetIssueUsageSummary query joins agent_task_queue filtered by
issue_id across all statuses. The existing partial index (migration 022)
only covers queued/dispatched rows, so completed tasks require a
sequential scan. Add a general index to prevent performance degradation
as task volume grows.
2026-04-10 14:34:32 +08:00
LinYushen
7620a5a7e9 fix(search): LOWER/LIKE for pg_bigm 1.2 index compatibility (#621)
* fix(search): use LOWER/LIKE instead of ILIKE for pg_bigm 1.2 compatibility

pg_bigm 1.2 on RDS does not support ILIKE index scans. Replace all
ILIKE expressions with LOWER(column) LIKE LOWER(pattern) so the GIN
indexes are utilized. Rebuild gin_bigm_ops indexes on LOWER() expressions.

Closes MUL-482

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

* fix(search): lowercase pattern in Go, add buildSearchQuery unit tests

- Lowercase phrase/terms in Go (strings.ToLower) so SQL only needs
  LOWER() on the column side, avoiding redundant per-query LOWER() on
  the pattern
- Add 5 unit tests for buildSearchQuery asserting SQL shape: no ILIKE,
  LOWER on columns only, lowercased args, multi-term AND, number match,
  include-closed flag, special char escaping

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:29:00 +08:00
CheinTian
289e3c3ad0 feat(agents): enable changing runtime (#617) 2026-04-10 13:54:46 +08:00
Jiayuan Zhang
abe005b403 feat(dx): add make dev one-command local setup
Simplifies local development from 3+ commands to a single `make dev`
that auto-detects environment (main/worktree), creates env files,
installs dependencies, starts PostgreSQL, runs migrations, and launches
both backend and frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:51:07 +08:00
Naiyuan Qing
e867076bde Merge pull request #616 from multica-ai/refactor/extract-chat-and-shared-ui
refactor: extract chat to shared packages + cleanup
2026-04-10 11:36:17 +08:00
Naiyuan Qing
303a4b3144 chore(ui): configure shadcn at packages/ui level
Add components.json to packages/ui so shadcn components can be installed
directly into the shared UI package instead of going through apps/web.
Add a root pnpm ui:add script as the canonical install command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:33:49 +08:00
Naiyuan Qing
0998a3a87d fix(desktop): allow tab buttons to receive clicks above drag region
Move WebkitAppRegion="no-drag" from the tab bar container to individual
buttons (TabItem and NewTabButton). This lets the empty space between
tabs remain part of the window drag region while still making the tabs
themselves clickable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:33:43 +08:00
Naiyuan Qing
5878bddd6b refactor(core): move my-issues view store to packages/core/issues/stores
The my-issues view store is shared client state that doesn't depend on
any UI library. Move it from packages/views/my-issues/stores/ to
packages/core/issues/stores/ to follow the no-duplication rule and keep
state factories together with related issue stores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:33:37 +08:00
Naiyuan Qing
102831919c refactor(chat): address code review feedback
- Document wsId/header coupling in chat queries (cache key vs API call)
- Extract finalizePending helper to reduce duplication across 4 WS handlers
- Store chat store handle in module-level variable for consistency with
  auth/workspace stores in CoreProvider
- Remove redundant ./chat/store package export (covered by ./chat barrel)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:28:12 +08:00
Naiyuan Qing
1dd8ca86c3 chore(web): remove unused Spinner, LoadingIndicator, and ThemeToggle
These components had zero consumers in the entire repo. Verified by
grep across both apps and all shared packages — they were dead code
left over from earlier iterations. The shadcn ui/spinner.tsx in
packages/ui is a separate component (Loader2-based) and is unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:14:52 +08:00
Naiyuan Qing
aa6577c5b7 refactor(chat): extract chat data layer to packages/core/chat
Move chat queries, mutations, and store from apps/web/core/chat/ and
apps/web/features/chat/store.ts to packages/core/chat/. Refactor store
to use createChatStore({ storage }) factory pattern (mirrors auth store)
so it works in both web (localStorage) and desktop (Electron) without
direct browser API access. Register chat store in CoreProvider.initCore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:14:36 +08:00
Naiyuan Qing
ef1db9e754 Merge pull request #613 from multica-ai/feat/tab-persist-and-polish
feat(desktop): tab persistence + last-tab close button fix
2026-04-10 10:50:04 +08:00
Naiyuan Qing
2d8c0a2d60 fix(desktop): hide close button when only one tab remains
Prevent showing the X button on hover for the last tab, since closing
it just replaces with a default tab — misleading UX.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:47:55 +08:00
Naiyuan Qing
5647c129da feat(desktop): persist tab state across app restarts
Add Zustand persist middleware to tab store so open tabs survive app
restarts. Uses merge callback to rebuild memory routers from persisted
paths on rehydration. History stacks start fresh (matches browser
"restore tabs" behavior).

- partialize: strips router/historyIndex/historyLength (not serializable)
- merge: recreates routers via createTabRouter(path), validates activeTabId
- version: 1 for future migration support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:47:44 +08:00
Naiyuan Qing
254871635e Merge pull request #612 from multica-ai/feat/per-tab-memory-router
feat(desktop): per-tab memory router + test infrastructure + CLAUDE.md rewrite
2026-04-10 10:38:06 +08:00
Naiyuan Qing
cb81aa48d3 feat(desktop): add project detail route
Wire /projects/:id in desktop router with ProjectDetailPage wrapper
(dynamic document title). Add FolderKanban icon mapping for project
tabs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:35:02 +08:00
Naiyuan Qing
6340b560c7 docs: rewrite CLAUDE.md — remove code details, add decision principles
Strip ~150 lines of code-level details (module tables, file trees,
import examples) that get outdated. Add no-duplication rule, test
architecture principles, and TDD workflow guidance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:34:56 +08:00
Naiyuan Qing
cc5e2e1712 test(views): rewrite shared component tests in packages/views
Move test ownership to where the code lives. LoginPage (28 tests),
IssuesPage (6 tests), IssueDetail (10 tests) now tested in
packages/views without framework-specific mocks. Old web tests
for shared components removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:34:49 +08:00
Naiyuan Qing
b067eee487 chore: set up test infrastructure for shared packages
Add vitest configs to packages/core and packages/views. Test deps
added to pnpm catalog for unified versioning. Web test deps migrated
to catalog references. pnpm test now discovers all packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:34:03 +08:00
Naiyuan Qing
1f9ce6582c refactor(desktop): update shell, tab-bar, and login for tab-based architecture
DesktopLayout → DesktopShell, AppContent handles auth routing at top
level, tab-bar and tab-sync adapted for per-tab memory routers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:33:56 +08:00
Naiyuan Qing
a4383e051f refactor(desktop): per-tab memory router with Activity-based state preservation
Each tab gets its own createMemoryRouter instance. React Activity API
preserves DOM and React state for hidden tabs. Navigation adapters
split into root-level (sidebar/modals) and per-tab providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:33:48 +08:00
Naiyuan Qing
c1b1a55808 Merge pull request #609 from multica-ai/fix/cross-platform-auth-search
refactor: extract shared cross-platform components
2026-04-10 09:50:12 +08:00
Naiyuan Qing
547b8839b2 refactor(auth): consolidate web login into shared LoginPage component
Extend shared LoginPage with CLI callback, workspace preference, and
token callback props. Web login page reduced from 393 lines to 52-line
thin wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:45:17 +08:00
Naiyuan Qing
4c88a1318d chore(web): remove dead markdown component directory
The entire apps/web/components/markdown/ directory was unused —
all consumers already import from @multica/views/common/markdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:44:57 +08:00
Naiyuan Qing
fb1554c0bf refactor(layout): extract DashboardGuard as shared guard + provider wrapper
Both web and desktop had independent guard + WorkspaceIdProvider logic.
Extract into a single DashboardGuard component so future changes only
need one update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:43:18 +08:00
Naiyuan Qing
33768a2d3a fix(runtimes): accept wsId as parameter instead of requiring WorkspaceIdProvider
useMyRuntimesNeedUpdate and useUpdatableRuntimeIds now take wsId as an
argument so they work safely outside WorkspaceIdProvider (e.g. in sidebar).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:27:47 +08:00
Naiyuan Qing
05067f4960 refactor(search): extract search to packages/views for cross-platform reuse
Moved SearchCommand, SearchTrigger, and search store from apps/web/features/
to packages/views/search/. Replaced useRouter (next/navigation) with the
existing useNavigation() abstraction. Wired search into desktop layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:27:36 +08:00
Naiyuan Qing
715f196434 fix(auth): wire onLogout callback to auth store and let guard handle redirect
CoreProvider.initCore() was not passing onLogin/onLogout to createAuthStore,
so the web cookie was never cleared on logout. The sidebar also hardcoded
push("/") which redirected to /issues on desktop via the index route.

Now the guard handles platform-specific redirect (web→"/", desktop→"/login").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:27:16 +08:00
Naiyuan Qing
add8bf9f4f Merge pull request #608 from multica-ai/feat/desktop-app
feat(desktop): add Electron desktop app + monorepo extraction
2026-04-10 08:30:09 +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
a8c3137f3b Merge remote-tracking branch 'origin/main' into feat/desktop-app
# Conflicts:
#	apps/web/app/(dashboard)/layout.tsx
#	apps/web/app/globals.css
#	apps/web/app/layout.tsx
#	apps/web/core/chat/mutations.ts
#	apps/web/core/chat/queries.ts
#	apps/web/features/chat/components/chat-message-list.tsx
#	apps/web/features/chat/components/chat-window.tsx
#	apps/web/features/landing/components/landing-footer.tsx
#	packages/core/package.json
#	packages/views/layout/app-sidebar.tsx
2026-04-10 08:01:19 +08:00
Naiyuan Qing
79b4c75303 fix: pre-resolve merge conflicts with origin/main
Prepare for merge by integrating main's new features into the
extracted shared packages architecture:
- Chat feature (ChatFab, ChatWindow) added to web dashboard extra slot
- Sidebar redesign (3-group nav, search slot, user footer, runtime updates)
- WorkspaceIdProvider moved outside SidebarInset for extra components
- Social links, twitter metadata, showDevtools, latestCliVersion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:59:29 +08:00
Naiyuan Qing
18b16f2936 docs: trim CLAUDE.md — remove implementation details, keep development conventions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:43:31 +08:00
Naiyuan Qing
8567dacd55 docs: update CLAUDE.md with desktop app architecture and cross-platform development guide
- Add monorepo tooling section (pnpm catalog, Turborepo, Internal Packages pattern)
- Document apps/desktop/ full structure (tab system, navigation adapter, build config)
- Add NavigationAdapter API documentation with openInNewTab/getShareableUrl
- Add cross-platform development rules (how to add pages, wire routes, handle titles)
- Document CSS architecture (shared imports, tokens, base styles, @source directives)
- Add desktop build commands (pnpm build, pnpm package, .env.production)
- Update package descriptions to reflect extracted modules (layout, auth, settings, agents, inbox)
- Update import conventions to include desktop patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:39:52 +08:00
Naiyuan Qing
a012d912fe feat(desktop): add tab system with document.title sync + upgrade shared LoginPage
Tab system:
- Tab store with open/add/close/switch actions
- document.title as single source of truth for tab titles (MutationObserver)
- Route-level default titles via react-router handle.title + TitleSync
- useDocumentTitle hook for dynamic titles (e.g. issue detail)
- Tab bar with fixed-width tabs, fade mask, hover-to-close

Login upgrade:
- Upgrade shared LoginPage with InputOTP, cooldown resend, Google OAuth support
- Google OAuth controlled via optional google prop (desktop omits it)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:05:07 +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
Jiayuan Zhang
02cdfcb93f feat(search): improve ranking with ILIKE, identifier search, multi-word support (#601)
* feat(search): improve ranking with ILIKE, identifier search, multi-word support

- Replace LIKE with ILIKE for case-insensitive matching
- Support identifier search (e.g. "MUL-123" or bare "123")
- Refine sorting tiers: number match > exact title > title starts with >
  title contains > all words in title > description > comment
- Add status-based tiebreaker (active issues rank higher)
- Support multi-word search where all terms must match somewhere
- Move search query from sqlc to dynamic SQL for flexibility

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

* fix(search): fix parameter type error for single-word queries

Only allocate per-term SQL parameters when there are multiple search
terms. For single-word queries, the phrase parameter already covers
the search — unused term params caused PostgreSQL error
"could not determine data type of parameter $3".

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:43:33 +08:00
Jiayuan Zhang
25080c6719 feat(chat): add session history panel to view archived conversations (#602)
Support viewing historical/archived chat sessions in the Master Agent chat
window. Previously, only active sessions were visible and archived ones were
permanently hidden.

Changes:
- Add ListAllChatSessionsByCreator SQL query (no status filter)
- Add ?status=all query param to GET /api/chat/sessions endpoint
- Add history button in chat header that opens a session list panel
- Sessions grouped by Active/Archived with archive action on active ones
- Clicking an archived session loads its messages in read-only mode
- Chat input disabled with "This session is archived" placeholder

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:40:55 +08:00
Jiayuan Zhang
89fd2ce96e refactor(views): reuse AssigneePicker in CreateIssueModal (#599)
* refactor(views): reuse AssigneePicker in CreateIssueModal

Replace the hand-rolled inline assignee Popover in CreateIssueModal with
the shared AssigneePicker component. This fixes missing features (private
agent permission checks, lock icon, disabled state, selection checkmark)
and ensures consistent behavior across all assignee dropdowns.

* refactor(views): consolidate all picker components across the codebase

Enhance shared pickers (StatusPicker, PriorityPicker, DueDatePicker,
ProjectPicker) with triggerRender, controlled open/onOpenChange, and
align props — matching the AssigneePicker API.

Replace inline implementations in:
- create-issue.tsx: Status, Priority, DueDate, Project (4 pickers)
- issue-detail.tsx sidebar: Status, Priority (2 pickers)
- batch-action-toolbar.tsx: Status, Priority (2 pickers)

StatusPicker now has its first consumer (was defined but unused).
Removes ~200 lines of duplicated picker code.
2026-04-10 02:18:49 +08:00
Jiayuan Zhang
7d5db1ce8b feat(sidebar): redesign layout for better space and grouping (#597)
* feat(sidebar): redesign sidebar layout for better space usage and grouping

- Split header into two rows: workspace switcher (full width) + search bar with new issue button
- Regroup navigation: Personal (Inbox, My Issues) + Workspace with label (Issues, Projects, Agents, Runtimes, Skills)
- Move Settings to SidebarFooter (like Linear)
- Search now renders as a full-width input-style button with ⌘K hint

Closes MUL-441

* fix(sidebar): style ⌘K shortcut as bordered badge matching project conventions

Use bordered kbd badge (bg-muted, border, font-mono) consistent with
search-command.tsx pattern. Render ⌘ symbol slightly larger for readability.

* feat(sidebar): add user profile info to footer

Show user avatar, name and email at the bottom of the sidebar
with a dropdown menu for logout, similar to the Lumis reference design.

* refactor(sidebar): move Settings back to Workspace nav, footer shows only user info

Settings is a navigable page that belongs with other nav items.
Footer now cleanly separates identity (user profile) from navigation.

* refactor(sidebar): split Workspace into Workspace + Configure groups

Split 6-item Workspace group into two cleaner groups:
- Workspace: Issues, Projects, Agents (core collaboration)
- Configure: Runtimes, Skills, Settings (infrastructure/admin)

* fix(sidebar): align search bar with nav items

Remove extra px-2 from search container and change button px-2.5 to px-2
so the search icon aligns at the same left offset as nav item icons.

* refactor(sidebar): make search and new issue regular menu items

Replace bordered input-style search bar and icon button with
SidebarMenuButton components so they share the same visual weight,
padding, and hover behavior as all other nav items.
2026-04-10 02:15:44 +08:00
Jiayuan Zhang
825e40358b feat(search): highlight matching keywords in search results (#598)
Add a HighlightText component that highlights the search query in both
issue titles and comment snippets using case-insensitive matching with
yellow highlight styling for light and dark modes.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:49:19 +08:00
Jiayuan Zhang
b5cccc8ac6 feat(landing): add OpenClaw and OpenCode to landing page (#596)
* feat(landing): add OpenClaw and OpenCode to landing page

The landing page hero "Works with" section and i18n text only listed
Claude Code and Codex. Updated to include all four supported runtimes:
Claude Code, Codex, OpenClaw, and OpenCode.

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

* feat(landing): remove X (Twitter) button from header nav

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:07:46 +08:00
Bohan Jiang
aec07456fc fix(realtime): add PAT auth support to WebSocket endpoint (#568) (#587)
The /ws endpoint only accepted JWT tokens while REST /api/* routes
accepted both JWTs and PATs (mul_*). Add PATResolver interface and
wire it into HandleWebSocket so PAT holders can use WebSocket streaming.

Also update README (en + zh-CN) to list OpenClaw and OpenCode as
supported agent runtimes alongside Claude Code and Codex.
2026-04-09 19:18:57 +08:00
Bohan Jiang
6209e2f3ae fix(server): allow deleting runtimes when all bound agents are archived (#589)
Previously, runtimes could never be deleted once an agent was created
because agents can only be archived (not deleted) and the count check
included archived agents. Now the check only counts active agents, and
archived agents are cleaned up before runtime deletion.
2026-04-09 19:17:54 +08:00
Naiyuan Qing
0a5a3b2450 Merge pull request #584 from multica-ai/NevilleQingNY/search-btn-ghost
fix(web): use ghost style for sidebar search button
2026-04-09 18:45:37 +08:00
Naiyuan Qing
90b2cb7848 fix(web): use ghost style for sidebar search button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:44:34 +08:00
Naiyuan Qing
bb34bd3db9 Merge pull request #583 from multica-ai/NevilleQingNY/sidebar-search-btn
feat(web): add search button to sidebar header
2026-04-09 18:39:55 +08:00
Naiyuan Qing
7950ac72af feat(web): add search button to sidebar header + restore turbo globalEnv
Add a visible search trigger button next to the create-issue button in
the sidebar header, improving search discoverability (previously only
accessible via ⌘K). Search dialog open state is shared via a Zustand
store so both the button and keyboard shortcut work.

Also restores turbo.json globalEnv config (FRONTEND_PORT, etc.) that was
accidentally dropped during the monorepo extraction, fixing worktree
port conflicts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:35:22 +08:00
Bohan Jiang
db55b79aa1 fix(web): align changelog versions with GitHub release tags (#582)
* docs(web): add v0.1.9 changelog entry for 2026-04-08

* docs(web): add v0.1.10 changelog entry for 2026-04-09

* fix(web): align changelog versions with GitHub release tags
2026-04-09 18:29:38 +08:00
LinYushen
21484e506a fix(realtime): re-subscribe WS handlers when client reconnects (#580)
subscribe/onReconnect used wsRef (a ref) with empty useCallback deps,
so the function identity never changed when the WSClient was recreated.
Consumers' effects never re-ran, leaving handlers registered on the
old (disconnected) client.

Switch to wsClient state so the callback identity updates on reconnect,
causing all useEffect consumers to re-subscribe on the new client.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:24:22 +08:00
Bohan Jiang
63d01f5d6c docs(web): add v0.1.10 changelog entry (#572)
* docs(web): add v0.1.9 changelog entry for 2026-04-08

* docs(web): add v0.1.10 changelog entry for 2026-04-09
2026-04-09 18:19:11 +08:00
yushen
6fa68fe20e fix(chat): set pendingTask before invalidating queries
Move setPendingTask() before invalidateQueries() so that
pendingTaskRef is set earlier, reducing the window where incoming
WS task:message events would be dropped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:08:25 +08:00
Jiayuan Zhang
141d7fd0aa feat: add official X (@multica_hq) links across repo and landing page (#577)
- README.md / README.zh-CN.md: add X link to top navigation
- layout.tsx: add twitter site/creator metadata (@multica_hq)
- Landing header: add X icon button next to GitHub
- Landing footer: add X and GitHub social icons
- Footer i18n: replace Community link with X (Twitter) in en/zh
- shared.tsx: add twitterUrl constant and XMark icon component

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:46:12 +08:00
LinYushen
c057741e22 Merge pull request #547 from multica-ai/agent/cc-girl/16ef1984
feat(chat): add agent chat feature
2026-04-09 17:27:34 +08:00
yushen
5ebadefcd7 Merge remote-tracking branch 'origin/main' into agent/cc-girl/16ef1984 2026-04-09 17:23:43 +08:00
LinYushen
70aea76bf6 fix(views): remove background container from provider logos (#573)
Show provider logos directly without the green/gray rounded background
container in both runtime list and detail views.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:22:46 +08:00
yushen
fb475915c1 fix(chat): add workspace scoping, error logging, and query cleanup
- CancelTaskByUser: verify task belongs to current workspace for both
  chat and issue tasks, preventing cross-workspace cancellation
- Log errors for TouchChatSession and CreateChatMessage instead of
  silently discarding them
- Add ON DELETE CASCADE to chat_session.creator_id FK
- Add staleTime: Infinity to chat query options (project convention)
- Remove dead useSendChatMessage mutation (replaced by direct api call)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:18:14 +08:00
yushen
1f717c9059 feat(chat): add ownership checks, optimistic messages, and cleanup
- Add creator ownership verification on chat session endpoints (get, archive, send, list messages)
- Add CancelTaskByUser handler with ownership check instead of unrestricted CancelTask
- Show user messages optimistically before server response
- Remove unused streamingContent from chat store and sendMessage mutation import
- Make QueryProvider devtools flag a prop instead of reading process.env in core package
- Add proper FK constraint on chat_session.creator_id → user(id)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:13:14 +08:00
yushen
8a73251b15 Merge remote-tracking branch 'origin/main' into agent/cc-girl/16ef1984 2026-04-09 17:06:37 +08:00
LinYushen
c283288133 feat(web): display provider logos in runtime list (#571)
* feat(web): display provider-specific logos in runtime list

Replace generic monitor/cloud icons with distinctive SVG logos for each
agent CLI provider (Claude, Codex, OpenCode, OpenClaw) in the runtime
list and detail views.

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

* fix(web): use official provider logos from upstream sources

Replace hand-drawn SVG approximations with official logos:
- Claude: Anthropic mark from Bootstrap Icons (bi-claude)
- Codex: OpenAI mark from Bootstrap Icons (bi-openai)
- OpenCode: pixel-art "O" from anomalyco/opencode brand assets
- OpenClaw: pixel lobster mascot from openclaw/openclaw

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:05:18 +08:00
Bohan Jiang
c8f0f3dc9d feat(views): show sub-issue progress in list rows (#566)
* feat(views): show sub-issue progress indicator in issue list rows

When an issue has sub-issues, display a circular progress ring with
done/total count (e.g. "2/3") in the list row. Progress is computed
from the already-loaded issue list without additional API calls.

Extracts ProgressRing into a shared component reused by both
issue-detail and list-row.

* feat(views): refine sub-issue progress UI and add to board view

- Move progress badge right after issue title (not pushed to far right)
- Increase progress ring size from 11px to 14px for better visibility
- Add sub-issue progress indicator to board card view
- Thread childProgressMap through BoardView → BoardColumn → BoardCard
2026-04-09 16:52:12 +08:00
yushen
821b6ece57 merge: resolve conflicts with main (project feature)
Merge both chat and project types/events/routes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:49:55 +08:00
yushen
3ffebd097c feat(chat): improve chat UI, fix streaming, add stop/fullscreen/agent permissions
- Redesign chat UI: Linear-style FAB, agent selector, empty state, Markdown rendering
- Fix WS message broadcast for chat tasks (resolve workspaceID from chat_session)
- Fix streaming race condition using refs for pendingTaskId
- Save assistant replies to chat_message on task completion
- Add real-time timeline rendering (tool calls, results, thinking) with collapsible groups
- Add historical timeline loading for past assistant messages
- Persist activeSessionId in localStorage + auto-restore from server
- Add chat workspace context to agent prompt (CLI commands, repos, skills)
- Add stop button (cancel task) during agent execution
- Add fullscreen mode (right-side panel, 50% width)
- Filter agent selector by visibility permissions (same as assign picker)
- Add generic POST /api/tasks/{taskId}/cancel route for chat tasks
- Add new chat (+) button, remove duplicate close button
- Devtools toggle via NEXT_PUBLIC_DEVTOOLS env var

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:47:11 +08:00
Naiyuan Qing
d911cdf5ac refactor: extract all shared logic to packages — apps are now thin routing shells
- Add CoreProvider to @multica/core/platform — single component for API/stores/WS/QueryClient init
- Delete 13 platform files across web (6) and desktop (7), each app keeps only navigation.tsx
- Extract AppSidebar + DashboardLayout to @multica/views/layout
- Extract LoginPage to @multica/views/auth
- Extract AgentsPage (1,279 lines) to @multica/views/agents (11 files)
- Extract InboxPage (468 lines) to @multica/views/inbox (5 files)
- Extract SettingsPage + 6 tabs (1,277 lines) to @multica/views/settings (9 files)
- Fix AppLink to use forwardRef for Base UI render prop compatibility
- Fix Tailwind @source to scan .ts files (status config with bg-info/bg-warning)
- Suppress next-themes React 19 script tag warning
- Add WebProviders wrapper for Server→Client function passing
- Wire all desktop routes to shared views, remove PlaceholderPage
- Net: +106 / -4,094 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:45:41 +08:00
Bohan Jiang
245beed829 feat(projects): add priority attribute to projects (#565)
Add priority field (urgent/high/medium/low/none) to projects, matching
the existing issue priority system. Includes database migration, API
support for create/update/list filtering, and UI for the create dialog,
project list table, and project detail page.
2026-04-09 16:31:05 +08:00
Bohan Jiang
741247c5cc fix(projects): add distinct colored dots for each project status (#564)
Each project status now displays a unique colored dot indicator in both
the status dropdown trigger and menu items. Previously all statuses
showed the same color, making them indistinguishable.
2026-04-09 16:23:17 +08:00
Naiyuan Qing
ef11bcd2d1 Merge pull request #536 from multica-ai/agent/naiyuan-agent/66842ca3
fix: upload content-type, disposition, attachment sync, and list API optimization (MUL-410)
2026-04-09 16:23:15 +08:00
Bohan Jiang
9da6a911cd fix(views): resolve nested button hydration error in agent live card (#563)
Change inner <button> to <span role="button"> inside CollapsibleTrigger
to fix "button cannot be a descendant of button" hydration error.
2026-04-09 16:16:01 +08:00
Bohan Jiang
b669b1c3a6 Merge pull request #562 from multica-ai/agent/j/4fbf073a
feat(cli): add project commands and --project flag for issues
2026-04-09 16:05:05 +08:00
Bohan Jiang
8c51614cfa Merge pull request #561 from multica-ai/feat/create-issue-project-picker
feat(issues): add project picker to create issue modal + fix IssuesHeader store
2026-04-09 16:04:46 +08:00
Jiang Bohan
1b7c3d7d94 fix(projects): remove issue count from Issues tab and align tab bar with header 2026-04-09 16:02:53 +08:00
Jiang Bohan
b7ffba4d2f fix(issues): wrap IssuesHeader inside ViewStoreProvider
IssuesHeader was rendered outside ViewStoreProvider in IssuesPage,
causing "useViewStore must be used within ViewStoreProvider" crash
after switching IssuesHeader to context-based store. Moved the
provider boundary up to include IssuesHeader.
2026-04-09 15:59:39 +08:00
Jiang Bohan
072ccc90aa feat(cli): add project commands and --project flag for issues
Add `multica project` CLI commands (list, get, create, update, delete,
status) so agents can manage projects. Also add --project flag to
`issue create` and `issue update` for associating issues with projects.
2026-04-09 15:57:05 +08:00
Jiang Bohan
8cf27af3b2 feat(issues): add project picker to create issue modal + fix IssuesHeader view store
- Add a Project pill to the create issue modal property toolbar,
  allowing users to assign a project at creation time. Uses the
  existing projectListOptions query and passes project_id in the
  create request. Supports selecting, changing, and clearing project.
- Fix IssuesHeader to use context-based useViewStore instead of the
  global useIssueViewStore singleton, so filters/sort/view toggle
  work correctly when mounted inside a project-scoped ViewStoreProvider.
2026-04-09 15:52:36 +08:00
Naiyuan Qing
0696532a99 fix(issues): skip list cache as initialData when description is missing
The list API no longer returns description. ContentEditor reads
defaultValue on mount only and ignores subsequent prop changes in
editable mode. Seeding initialData from list cache (description=null)
caused the editor to mount with empty content permanently.

Only use list cache as initialData when description is present;
otherwise let the loading state show until the detail query resolves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:45:25 +08:00
Bohan Jiang
0ff9e2ba39 Merge pull request #558 from multica-ai/feat/project-ui-redesign
feat(projects): redesign project UI to match Linear
2026-04-09 15:38:56 +08:00
Bohan Jiang
3916a0ed1d Merge pull request #557 from multica-ai/agent/j/e8ad55f1
fix(cli): show actionable error when workspace_id is missing
2026-04-09 15:35:48 +08:00
Jiang Bohan
b6c369ef17 feat(projects): redesign project UI to match Linear and align with issue patterns
Create Project dialog:
- Match Create Issue modal layout (custom shell, TitleEditor,
  ContentEditor, property toolbar with pill buttons)
- Add status picker, lead picker, and emoji icon chooser
- Expandable dialog (compact ↔ expanded)

Projects list page:
- Replace card layout with Linear-style table (column headers,
  dense rows with icon, name, status badge, lead avatar, created date)

Project detail page:
- Linear-style breadcrumb header with ... menu (copy link, delete)
  and copy link icon on the right
- Tab bar: Overview + Issues
- Overview: clickable emoji icon picker, TitleEditor, inline property
  pills (status + lead), ContentEditor for description
- Issues tab: reuses existing BoardView/ListView/IssuesHeader/
  BatchActionToolbar with a project-scoped view store and client-side
  project_id filtering
- Remove summary stats section
2026-04-09 15:35:32 +08:00
Naiyuan Qing
870d9d9465 docs: add implementation plan for upload/attachment fixes (MUL-410)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:33:19 +08:00
Naiyuan Qing
fee8f41ea5 perf(api): omit description from list issues response
Change ListIssues and ListOpenIssues SQL queries to select specific
columns (excluding description, acceptance_criteria, context_refs).
Reduces list API payload size, especially for issues with embedded images.

Frontend handles null description gracefully — board card short-circuits,
issue detail fetches full data via its own query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:33:15 +08:00
Naiyuan Qing
80afd1cc00 fix(editor): decouple description uploads from attachment records
Description editor uploads no longer pass issueId to the upload API.
This avoids stale attachment records when users delete images from
the editor — the URL already lives in the markdown content.

Comment/reply uploads continue linking to the issue for agent discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:33:08 +08:00
Naiyuan Qing
8526f013da fix(upload): SVG content-type fallback and Content-Disposition for non-media files
- Add extension-based content-type override after http.DetectContentType()
  to fix SVG files getting text/xml instead of image/svg+xml
- Use Content-Disposition: attachment for non-media files so browsers
  download CSV/PDF instead of displaying inline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:33:02 +08:00
Jiang Bohan
3046f51300 fix(cli): show actionable error when workspace_id is missing
When a user has multiple workspaces but no default configured,
`agent list` and `issue list` would fail with a cryptic server-side
"workspace_id is required" error. Now the CLI validates early and
suggests using --workspace-id, MULTICA_WORKSPACE_ID env, or
`multica config set workspace_id`.

Closes #532
2026-04-09 15:31:54 +08:00
LinYushen
d5f18c23cb fix(runtime): remove redundant provider from list item subtitle (#555)
The runtime name already includes the provider (e.g., "Codex (mini.local)"),
so showing provider again in the subtitle was redundant. Now the subtitle
shows only the owner avatar + name, falling back to runtime_mode if no owner.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:17:22 +08:00
Bohan Jiang
dab9c7cf9b Merge pull request #553 from gyh1621/gyh
fix(daemon/repocache): unstick stale cache from initial snapshot
2026-04-09 15:05:48 +08:00
Naiyuan Qing
83769c4780 fix(desktop): add type=submit to login buttons
base-ui Button defaults to type="button", which doesn't trigger form
onSubmit. Explicit type="submit" fixes the click-to-submit flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:01:23 +08:00
Bohan Jiang
68e2a14ba2 feat(projects): add Project entity with full-stack CRUD support (#552)
Implements the Project concept as a higher-level grouping for issues.
Hierarchy: workspace → project → issue → sub-issue.

Backend:
- Migration 034: project table + issue.project_id FK
- sqlc queries for project CRUD
- Project handler with list/get/create/update/delete
- Issue handler updated to support project_id in create/update
- Routes at /api/projects, WebSocket event constants

Frontend (new monorepo structure):
- @multica/core: Project types, API client methods, queries/mutations,
  status config, realtime sync
- @multica/views: Projects list page, detail page (overview + issues
  tabs), project picker for issue detail panel
- apps/web: Route pages, sidebar navigation entry

All TypeScript type checks and tests pass.
2026-04-09 14:59:16 +08:00
Naiyuan Qing
848d79df11 fix(desktop): remove type:module — Electron main/preload are CJS
Root cause: "type": "module" made Node.js treat all .js as ESM, but
Electron loads preload via require() (CJS). Removing it makes .js
default to CJS, which is what Electron expects. No rollup overrides needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:55:44 +08:00
Naiyuan Qing
1caa7f6324 fix(desktop): preload .cjs output for ESM package + CORS for electron dev
- Preload output as .cjs so Node.js treats it as CJS regardless of
  "type": "module" in package.json
- Add electron-vite dev server ports (5173, 5174) to default CORS origins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:53:15 +08:00
yushen
f9a430e100 merge: resolve conflicts with main branch monorepo extraction
Update chat feature imports to use new package paths:
- @/shared/types → @multica/core/types
- @/shared/api → @/platform/api
- @core/* → @multica/core/*
- @/features/realtime → @multica/core/realtime
- @/components/ui/* → @multica/ui/components/ui/*

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:49:47 +08:00
Bohan Jiang
d7a37f60b5 fix(web): resolve CSS token import and WorkspaceIdProvider crash after monorepo extraction (#551)
- globals.css: use relative path for @multica/ui/styles/tokens.css
  since Tailwind v4's @import resolver doesn't follow pnpm workspace
  symlinks + package.json#exports
- globals.css: widen @source globs from *.tsx to *.{ts,tsx} so
  Tailwind scans .ts config files — fixes bg-info being purged
  (Done badge invisible in light mode)
- layout.tsx: hoist WorkspaceIdProvider above SidebarProvider so
  AppSidebar (which now calls useWorkspaceId via useMyRuntimesNeedUpdate
  from #533) doesn't throw on mount
2026-04-09 14:48:48 +08:00
Naiyuan Qing
0e0c5f4cdb fix(desktop): force preload CJS output and fix CSS @source paths
- Preload must be CJS (Electron loads it via require), force format: "cjs"
  and entryFileNames: "[name].js" so output matches main's reference
- @source paths were 4 levels up but need 5 (src/renderer/src/ to root)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:47:46 +08:00
Naiyuan Qing
bea274492c fix(desktop): use localStorage instead of electron-store
Electron renderer IS a browser — localStorage works natively, no need
for electron-store in preload. Removes the preload module loading issue
and eliminates an unnecessary dependency + IPC bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:46:00 +08:00
Naiyuan Qing
f7c1ae4d77 fix(desktop): move AuthInitializer to App root to prevent init deadlock
AuthInitializer was inside DashboardShell which has an isLoading early
return — the initializer never rendered, so isLoading never became false.
Moved to App.tsx (same as web's root layout) so it always executes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:43:00 +08:00
Naiyuan Qing
784111a498 fix(desktop): fix tsconfig path alias and AppLink children type error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:37:33 +08:00
Naiyuan Qing
77f48d9f26 feat(desktop): add CSS, router, pages, and app entry with provider nesting
- globals.css with Tailwind + design tokens from @multica/ui
- Hash router with dashboard shell, issues, my-issues, runtimes, skills pages
- Login page with email OTP flow (no Google OAuth)
- IssueDetailPage wrapper extracting route param for IssueDetail
- App.tsx with ThemeProvider > QueryProvider > RouterProvider nesting
- main.tsx without StrictMode to avoid Zustand double-render issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:35:51 +08:00
gyh1621
6e475b9521 fix(daemon/repocache): unstick stale cache from initial snapshot
The bare cache used a mirror-style fetch refspec
(+refs/heads/*:refs/heads/*) which collided with worktree-locked
refs/heads/agent/<task> branches once those branches were pushed
back to origin as PRs. git fetch aborted with "refusing to fetch
into branch ... checked out at ...", the error was swallowed as a
warning, and every subsequent checkout reused the snapshot from
the original clone.

Fix:
- Clone / migrate bare caches to a remote-tracking layout
  (+refs/heads/*:refs/remotes/origin/*) so fetched heads never
  land in refs/heads/*.
- Resolve the base ref from refs/remotes/origin/HEAD with a
  5-level fallback (verified origin/HEAD symref to origin/main
  or origin/master to the bare HEAD bridged into origin/<same>
  to single-entry origin/* scan to bare HEAD for legacy caches).
- Refuse to guess when refs/remotes/origin/* has multiple
  candidates and none match a known fallback, so CreateWorktree
  fails loudly instead of basing work on an arbitrary branch.
- Refresh refs/remotes/origin/HEAD after every successful fetch,
  not just on the legacy migration path, so a cache that was
  already modern picks up an upstream default-branch change.
- Verify the primary symref target actually exists so a phantom
  refs/remotes/origin/HEAD from a broken set-head does not
  surface a deleted branch.
- Detect legacy caches on the fly and rewrite refspec +
  refs/remotes/origin/* + refs/remotes/origin/HEAD in place so
  existing clones self-heal on next use.
- Serialize per-bare-repo mutation (both Sync and CreateWorktree)
  with sync.Map-backed mutexes so concurrent fetch and worktree
  add on the same repo cannot race on git's own lockfiles.
- Narrow the already-exists retry to actual branch-collision
  errors so a path-collision no longer silently leaks a branch
  into the bare repo.
2026-04-09 14:34:51 +08:00
Naiyuan Qing
dafd51e327 feat(desktop): add title bar, dashboard shell, sidebar, and shared components
- multica-icon: copied from web, zero platform-specific deps
- theme-provider: next-themes + TooltipProvider wrapper
- title-bar: draggable frameless title bar with macOS traffic light inset
- app-sidebar: adapted from web — uses @multica/views/navigation instead of next/link
- dashboard-shell: root layout with auth guard, sidebar, outlet, and workspace provider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:33:20 +08:00
Naiyuan Qing
f9eeafb568 feat(desktop): add renderer platform layer — storage, api, auth, ws, navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:30:12 +08:00
Naiyuan Qing
4585306bfc feat(desktop): frameless window with hiddenInset title bar and electron-store preload bridge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:28:18 +08:00
LinYushen
0c4f1027e8 fix(runtime): redesign filter bar with segmented control and owner dropdown (#548)
Replace cluttered inline owner pills with a clean two-part filter bar:
- Left: Mine/All segmented control with proper bg-muted container
- Right: Owner DropdownMenu (only in All mode) with avatars and counts

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:27:36 +08:00
Naiyuan Qing
74cc1d488e chore(desktop): scaffold electron-vite desktop app with monorepo config
- Scaffold apps/desktop/ using electron-vite react-ts template
- Configure electron.vite.config.ts with externalizeDeps, React, Tailwind CSS v4
- Wire up @multica/core, @multica/ui, @multica/views workspace dependencies
- Configure electron-builder.yml for mac/linux/win packaging
- Add @tailwindcss/vite to pnpm catalog
- Add dev:desktop script and electron to onlyBuiltDependencies in root package.json
- Clean up generated boilerplate, keep minimal placeholder renderer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:26:30 +08:00
Bohan Jiang
a135c44838 fix(issues): add done issue pagination to list view (#545)
List view only showed the first 50 done issues without a total count or
load-more mechanism. Reuse the existing useLoadMoreDoneIssues hook and
extract InfiniteScrollSentinel into a shared component so both board and
list views paginate identically.
2026-04-09 14:21:15 +08:00
Bohan Jiang
ec2b48a616 feat(runtime): point-to-point update notifications via registered_by (#533)
* feat(runtime): proactive CLI update notifications with per-user filtering

- Add latestCliVersionOptions query (GitHub Releases API, 10-min TanStack cache)
- Add useMyRuntimesNeedUpdate / useUpdatableRuntimeIds hooks using owner_id
- Show red dot on sidebar Runtimes item when user's runtimes need updates
- Show update arrow icon alongside status dot in runtime list items

* fix(core): add runtimes/hooks to package.json exports
2026-04-09 14:20:54 +08:00
yushen
50f9e673e8 feat(chat): add agent chat feature (full stack)
Implement the Master Agent chat feature allowing users to chat with agents
directly from a floating window, separate from the issue-based workflow.

Backend:
- New chat_session and chat_message tables (migration 033)
- Make issue_id nullable on agent_task_queue for chat tasks
- REST API: create/list/get/archive sessions, send/list messages
- EnqueueChatTask in TaskService with session_id persistence
- WS events: chat:message, chat:done
- Daemon: chat task type with separate prompt builder
- ClaimTaskByRuntime populates chat context (session, message, repos)

Frontend:
- ChatSession/ChatMessage types + API client methods
- core/chat: TanStack Query options, mutations with optimistic updates, WS updaters
- features/chat: Zustand store, ChatFab (floating button), ChatWindow with
  real-time streaming via task:message events
- Mounted in dashboard layout (bottom-right corner)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:19:46 +08:00
LinYushen
e2d98181c7 feat(runtime): owner avatar display and owner filter (#542)
- Show owner avatar + name in runtime list items (replaces text-only)
- Show owner avatar + name in runtime detail info grid
- Add per-owner filter pills in "All" mode for quick filtering

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:10:22 +08:00
Bohan Jiang
a9e68abb9d fix(usage): add Codex session log scan for token usage (#544)
Codex doesn't expose token usage through its JSON-RPC app-server
protocol. The turn/completed and task_complete notifications don't
contain usage fields.

Fix: after Codex execution finishes, scan the on-disk session JSONL
files (~/.codex/sessions/YYYY/MM/DD/*.jsonl) for token_count events.
Only files modified after the task's start time are scanned, avoiding
counting unrelated sessions. This matches the same data format the
existing runtime_usage scanner reads.
2026-04-09 14:08:36 +08:00
Naiyuan Qing
7ca5a97ec8 Merge pull request #543 from multica-ai/docs/update-claude-md
docs: update CLAUDE.md for monorepo architecture
2026-04-09 14:08:10 +08:00
LinYushen
e3f34ace8e Merge pull request #541 from multica-ai/fix/pg-bigm-ci-migration
fix(search): make pg_bigm migration graceful for CI
2026-04-09 14:05:46 +08:00
Naiyuan Qing
a9b3d4e6f4 docs: update CLAUDE.md for monorepo architecture
Rewrite architecture section to reflect the three-package monorepo
structure (core/ui/views). Key changes:

- Replace old 4-layer structure (app/core/features/shared) with
  package architecture and platform bridge pattern
- Document store factory pattern (createAuthStore, createWorkspaceStore)
- Document StorageAdapter, NavigationAdapter abstractions
- Update import conventions (@multica/core, @multica/ui, @multica/views)
- Add package boundary rules section
- Update shadcn command for monorepo (npx shadcn add -c apps/web)
- Remove references to deleted dirs (shared/, core/ inside apps/web)
- Keep backend section unchanged (not affected by extraction)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:04:06 +08:00
yushen
a2a021a0dd fix(search): make pg_bigm migration graceful when extension unavailable
CI uses pgvector/pgvector:pg17 which doesn't ship pg_bigm. Wrap
CREATE EXTENSION and index creation in DO/EXCEPTION blocks so the
migration succeeds without pg_bigm — indexes are skipped and search
falls back to plain LIKE scans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:00:03 +08:00
Naiyuan Qing
711ab886e2 Merge pull request #539 from multica-ai/feat/monorepo-extraction
feat: monorepo extraction — packages/core + ui + views
2026-04-09 13:55:34 +08:00
Naiyuan Qing
a092443a09 merge: resolve conflicts with main (search + runtime owner/delete)
- Merge origin/main (4 commits: search, runtime owner, multi-agent fix)
- Migrate new search feature imports to monorepo paths
- Move new runtime mutations to packages/core/runtimes/
- Resolve 5 conflicts in layout, runtime components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:50:36 +08:00
Naiyuan Qing
de73d39310 fix: address code review — SSR safety, missing deps, stale config
Critical:
- Create webStorage adapter (SSR-safe localStorage wrapper)
- Replace bare localStorage in platform/auth.ts and platform/workspace.ts
- Add all missing dependencies to packages/views/package.json
  (sonner, @dnd-kit/*, @tiptap/*, recharts, lowlight, etc.)

Important:
- Delete duplicate apps/web/components/common/actor-avatar.tsx
  (identical to packages/views/common/actor-avatar.tsx)
- Update components.json aliases to point to @multica/ui/*
- Remove empty apps/web/shared/ directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:41:01 +08:00
LinYushen
ff27a249cc feat(runtime): add owner tracking, filtering, and delete (#535)
Add owner_id to agent_runtime table to track who registered each runtime.
Backend: new delete endpoint with role-based permissions (owner/admin can
delete any, members only their own), list filtering by owner (?owner=me),
and agent dependency check before deletion.
Frontend: Mine/All filter toggle in runtime list, owner display in list
items and detail view, delete button with AlertDialog confirmation.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:38:46 +08:00
Naiyuan Qing
4668aad039 refactor(core): remove platform coupling — StorageAdapter, sonner, barrel cleanup
P0: Replace all localStorage calls in packages/core with StorageAdapter
- Create StorageAdapter interface (getItem/setItem/removeItem)
- Auth store factory now requires storage parameter
- Workspace store factory accepts optional storage parameter
- WSProvider accepts storage prop for token retrieval
- apps/web/platform/ passes localStorage as the web implementation

P1: Remove sonner UI dependency from packages/core
- Replace toast.error() in workspace store with onError callback
- Move sonner import to apps/web/platform/workspace.ts
- Remove sonner from packages/core/package.json dependencies

P2: Delete 5 pure re-export barrel files in apps/web/features/
- features/issues/index.ts, modals/index.ts, navigation/index.ts,
  workspace/index.ts, inbox/index.ts — all had zero consumers
- features/ now only contains auth/ (web-only cookie + initializer)
  and landing/ (web-only pages)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:16:51 +08:00
yushen
b484b78cbd fix(search): use rune-based snippet slicing and fix dialog a11y
- extractSnippet now uses rune-based indexing to avoid splitting multi-byte
  UTF-8 characters (CJK safety)
- Move DialogHeader inside DialogContent for correct DOM/a11y structure
- Add cleanup useEffect for debounce timer and abort controller on unmount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:11:07 +08:00
LinYushen
23136da34f feat(search): implement full-text search for issues (#507)
* feat(search): implement full-text search for issues

Add pg_bigm-based full-text search across issue titles and descriptions,
with API endpoint, CLI subcommand, and web Cmd+K search dialog.

- Migration 032: pg_bigm extension + GIN indexes on title/description
- Server: GET /api/issues/search?q=... with pagination and total count
- CLI: `multica issue search <query>` with table/json output
- Web: Cmd+K command palette using cmdk, with debounced search

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

* fix(search): address review feedback on search implementation

1. Escape LIKE special characters (%, _, \) in handler to prevent
   matching anomalies from user input.
2. Wire AbortController signal into searchIssues fetch so in-flight
   requests are actually cancelled on new input.
3. Fix offset=0 falsy check — use !== undefined instead of truthiness.
4. Merge results + count into single query using COUNT(*) OVER()
   window function, eliminating the duplicate DB round-trip.
5. Exclude done/cancelled issues by default; add include_closed
   parameter to API, CLI (--include-closed), and web client.

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

* fix(search): default web search to include all statuses

Pass include_closed: true in the web Cmd+K search so results include
done and cancelled issues by default, matching the reviewer's request.

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

* feat(search): add comment search with snippet extraction

Extend search to cover issue comments in addition to title/description.
Results are deduplicated at the issue level, with match_source and
matched_snippet fields indicating where and what matched.

- Migration 033: pg_bigm GIN index on comment.content
- SQL: EXISTS subquery for comment matching, correlated subquery for
  snippet extraction, 3-tier ranking (title > description > comment)
- Server: SearchIssueResponse with match_source and matched_snippet
- Web: show comment icon + snippet below issue title when matched
- CLI: MATCH column shows source and truncated snippet

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

* feat(search): redesign search dialog to match Linear's spacious style

- Widen dialog from sm (384px) to xl (576px) with top-20% positioning
- Larger search input with icon, generous padding, and ESC hint
- Use cmdk primitives directly for full style control
- Taller result list (400px / 50vh), spacious result items (py-2.5)
- Rounded-lg items with accent highlight on selection
- Cleaner border separator between input and results

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:43:21 +08:00
Bohan Jiang
5d1cc2a9bb fix(web): multi-agent sticky card with expand/collapse (#516)
* fix(web): multi-agent sticky card with expand/collapse pattern

- Move sticky positioning to the wrapper div so the entire agent area
  sticks together instead of each card independently
- Show first agent card always visible, with "N more agents working"
  expand button for additional agents
- Remove scrollContainerRef prop (no longer needed with native sticky)
- Simplify SingleAgentLiveCard by removing auto-collapse-on-scroll logic

* fix(web): pin primary agent card to top and drop collapse UI

- Remove the mt-4 wrapper around AgentLiveCard in issue-detail so the
  sticky wrapper is a direct child of the Activity section — sticky now
  has a tall enough parent to stay pinned through TaskRunHistory and
  the full comment timeline
- Simplify multi-agent rendering: only the first running agent sticks
  to the top, any additional agents render below it and scroll with
  the page. Removes the expand/collapse "N more agents working" button
2026-04-09 12:36:43 +08:00
Naiyuan Qing
f41a0cf423 feat(views): extract packages/views — shared business UI + navigation adapter
- Create NavigationAdapter interface (push, replace, back, pathname, searchParams)
- Create AppLink component replacing next/link in 4 files
- Replace useRouter → useNavigation in 3 files (issue-detail, create-issue, create-workspace)
- Create WebNavigationProvider wrapping Next.js useRouter/usePathname/useSearchParams
- Move ~85 feature UI files (issues, editor, modals, my-issues, skills, runtimes) to packages/views/
- Add store singleton registration pattern (registerAuthStore, registerWorkspaceStore)
- Create data-aware wrappers in packages/views/common/ (ActorAvatar, Markdown)
- Update all app-layer imports to @multica/views/*
- Add @source directive for Tailwind to scan views package
- packages/views/ has zero next/* imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:49:55 +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
Naiyuan Qing
e1e7f68330 feat: extract packages/core — Turborepo infrastructure + headless business logic
Phase 1: Monorepo infrastructure
- Add Turborepo with turbo.json pipeline (build, dev, typecheck, test)
- Update pnpm-workspace.yaml to include packages/*
- Create shared TypeScript config (packages/tsconfig)

Phase 2: Extract packages/core (zero react-dom, all-platform reuse)
- Move domain types, API client, logger, utils → packages/core/
- Move TanStack Query modules (issues, inbox, workspace, runtimes)
- Move Zustand stores (auth, workspace, issues, navigation, modals)
- Move realtime sync (WSProvider, hooks, ws-updaters)
- Refactor auth/workspace stores to factory pattern for DI
- Refactor ApiClient with onUnauthorized callback
- Refactor useWorkspaceId to React Context (WorkspaceIdProvider)
- Refactor WSProvider to accept wsUrl + store props
- Create apps/web/platform/ bridge layer (api singleton, store instances)
- Update 91 import paths across apps/web/
- Fix 3 test files for new import paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:20:00 +08:00
Naiyuan Qing
e2da970344 Merge pull request #530 from multica-ai/feat/drag-upload
feat(editor): drag-and-drop file upload with file card
2026-04-09 09:32:48 +08:00
Naiyuan Qing
b3fa5557ca merge: resolve conflict with main (import useModalStore)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:29:12 +08:00
Naiyuan Qing
19a1bbba4a feat(editor): drag-and-drop file upload with file card display
- Add drag-and-drop overlay with brand color visual feedback
- Images: inline rendering with blob preview → real URL replacement
- Non-images: file card node (spinner → filename card with download button)
- File card markdown roundtrip: [name](url) ↔ fileCard node via preprocessor
- Fix: double-upload on drag (check defaultPrevented)
- Fix: drop overlay not clearing (global drop/dragend listener)
- Fix: drop replacing existing content (use posAtCoords for drop position)
- Fix: multi-file drop position drift (only first file uses drop pos)
- Fix: same-name file upload conflict (use uploadId instead of filename)
- Fix: image upload descendants traversal not stopping (add found flag)
- Fix: parent comment edit missing onUploadFile prop
- Remove: attachment section UI (files live in markdown)
- Remove: file type whitelist (accept all types like Linear)
- Remove: console.log perf logs from production code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:26:12 +08:00
Jiayuan Zhang
f57cf44eba Merge pull request #526 from multica-ai/forrestchang-patch-1
doc: remove license section
2026-04-09 03:11:17 +08:00
Jiayuan Zhang
ae797811d2 doc: remove license section
Removed License section from README.md
2026-04-09 03:11:03 +08:00
Jiayuan Zhang
7d01cf8c68 Merge pull request #525 from multica-ai/agent/emacs/readme-managed-agents
docs: position Multica as open-source managed agents platform
2026-04-09 03:09:45 +08:00
Jiayuan Zhang
e79eabcc18 docs: position Multica as open-source managed agents platform
- Update subtitle: "The open-source managed agents platform"
- Add managed agents positioning to "What is Multica?" section
- Add lifecycle summary line above Features list
- Mirror all changes in Chinese README

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:07:58 +08:00
Jiayuan Zhang
d2e4b9753d feat(issues): add fullscreen agent execution transcript view (#524)
* feat(issues): add fullscreen agent execution transcript view

Adds a new "expand" button (Maximize2 icon) to both the live agent card
and execution history entries. Clicking it opens a fullscreen dialog with:

- A colored timeline progress bar showing execution flow at a glance
  (green = agent text, violet = thinking, blue = tool calls,
   gray = results, red = errors)
- Detailed event list with type labels, summaries, and expandable detail
- Click-to-scroll: clicking a timeline segment scrolls to that event
- Copy-all button for the full transcript

Inspired by Anthropic's Cloud Managed Agents session transcript UI.

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

* feat(issues): add runtime and agent metadata to transcript dialog

Adds metadata chips to the transcript dialog header showing:
- Runtime provider (e.g., "Claude Code", "Codex")
- Runtime environment name + mode (local/cloud)
- Agent description
- Duration, tool count, event count, and creation time

Metadata is fetched on dialog open via existing API endpoints.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:58:04 +08:00
Jiayuan Zhang
fab17b48b3 Merge pull request #520 from multica-ai/license/refine-commercial-restriction
chore(license): refine commercial restriction to target SaaS/resale only
2026-04-08 23:48:22 +08:00
Jiayuan Zhang
4f8969ef52 chore(license): refine commercial restriction to target SaaS/resale only
Replace "multi-tenant environment" restriction with "hosted or embedded
service" restriction. Internal use with multiple workspaces is now
explicitly allowed. Only providing Multica as a hosted service to third
parties or embedding it in a commercial product requires a license.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:47:05 +08:00
Jiayuan Zhang
2e5b8b9a87 Merge pull request #518 from multica-ai/license/modified-apache-2.0
chore: update LICENSE to modified Apache 2.0
2026-04-08 21:19:54 +08:00
Jiayuan Zhang
f4ba27f2f5 chore: update LICENSE to modified Apache 2.0 with commercial restrictions
Replace standard Apache 2.0 with a modified version that adds:
- Multi-tenant SaaS restriction (requires commercial license)
- Frontend LOGO/copyright protection
- Contributor agreement for relicensing rights

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:15:58 +08:00
Bohan Jiang
e6f840ca11 chore(issues): shrink add sub-issue label and remove jump-to-bottom button (#515) 2026-04-08 19:07:35 +08:00
Bohan Jiang
aa770f2333 feat(issues): show "Add sub-issues" button when no sub-issues exist (#511)
Previously the sub-issues section only rendered when child issues were
present. This adds a Linear-style "+ Add sub-issues" button below the
description area so users can create sub-issues from an empty state.
2026-04-08 18:51:03 +08:00
Bohan Jiang
bd6731525e fix(issues): polish sub-issue UI and sync parent children cache (#506)
- Add Linear-style "Sub-issue of …" breadcrumb under the title with a
  parent progress ring
- Refresh sub-issues section: progress ring badge, identifier column,
  bordered list, collapse toggle, dashed assignee placeholder
- useUpdateIssue + onIssueUpdated WS handler now also patch and
  invalidate the parent's children query so sub-issue status/assignee
  changes show up on the parent page without a refresh
2026-04-08 17:04:55 +08:00
Bohan Jiang
68d052625c docs(web): add v0.1.9 changelog entry for 2026-04-08 (#504) 2026-04-08 17:03:50 +08:00
Bohan Jiang
3d053345fd perf(web): fix slow tab switching by removing dynamic root layout (#502)
The root layout called `await cookies()` to read the locale, which
marked the entire app as dynamic. In Next.js 16, dynamic pages have
Router Cache staleTime=0, causing a fresh RSC server roundtrip on
every navigation — the root cause of ~400ms tab switching delays.

- Remove cookies() from root layout, making it static
- Add LocaleSync client component to read locale cookie on the client
- Add loading.tsx skeleton for dashboard routes as a loading fallback
2026-04-08 16:49:25 +08:00
Bohan Jiang
180c6966db fix(issues): polish sub-issues section design to match Linear (#503)
- Add chevron collapse indicator in header
- Show completion progress (done/total) with tabular-nums
- Use left border indentation for child items (tree view)
- Increase icon size, row padding, and spacing
- Larger + button with better hover state
- Only show section when child issues exist
2026-04-08 16:47:07 +08:00
LinYushen
0c45864ef0 fix(board): show total count in Done column and infinite scroll (#501)
* fix(board): show total count in Done column header and auto-load on scroll

- Column header now shows server-side doneTotal instead of loaded count
- Replace "Load more" button with IntersectionObserver sentinel for
  infinite scroll in the Done column

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

* fix(board): move sentinel below imports and stabilize observer

- Move InfiniteScrollSentinel after all import statements
- Use callback ref to avoid recreating IntersectionObserver on every render

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

* fix(board): add optional chaining for IntersectionObserver entry

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:33:17 +08:00
Bohan Jiang
c6ba954eb8 fix(issues): move sub-issues to content area and fix real-time refresh (#500)
1. Move sub-issues section from sidebar to main content area (below
   description), matching Linear's layout. Shows status icon, title,
   and assignee avatar for each child issue.

2. Fix real-time refresh: invalidate parent's childIssuesOptions query
   in useCreateIssue mutation (onSuccess), onIssueCreated WS handler,
   and onIssueDeleted WS handler so sub-issues list updates immediately
   without page refresh.
2026-04-08 16:31:49 +08:00
LinYushen
76354cd968 fix(board): show total count in Done column and infinite scroll (#498)
* fix(board): show total count in Done column header and auto-load on scroll

- Column header now shows server-side doneTotal instead of loaded count
- Replace "Load more" button with IntersectionObserver sentinel for
  infinite scroll in the Done column

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

* fix(board): move sentinel below imports and stabilize observer

- Move InfiniteScrollSentinel after all import statements
- Use callback ref to avoid recreating IntersectionObserver on every render

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:15:15 +08:00
Bohan Jiang
4bdb86057e fix(issues): use TanStack Query for sub-issue data fetching (#499)
The sub-issue code was using direct `api` calls, but the codebase was
refactored to TanStack Query and the `api` import was removed from
issue-detail.tsx, causing a build error on Vercel.

Replace useState+useEffect with useQuery for both parent and child
issue fetching, consistent with the TQ migration.
2026-04-08 16:10:40 +08:00
Bohan Jiang
a8a8ff6eca feat(issues): add sub-issue support (#483)
* feat(issues): add sub-issue support

- Backend: Add ListChildIssues SQL query, add parent_issue_id to UpdateIssue,
  add GET /api/issues/{id}/children endpoint
- Frontend: Display parent issue breadcrumb and link in issue detail sidebar,
  show child issues list with status icons, add "Create sub-issue" action in
  dropdown menu and sidebar, pass parent_issue_id through create issue modal
- Update test mocks for new API method

* fix(issues): add parent validation, cycle detection, and improve child refresh

- CreateIssue: validate parent issue exists in the same workspace
- UpdateIssue: validate parent exists, prevent self-referencing, detect
  circular parent chains (up to 10 levels deep)
- Frontend: derive child issues from store when available instead of
  refetching on every global issue count change
2026-04-08 15:57:13 +08:00
Naiyuan Qing
0dcaa60919 Merge pull request #496 from multica-ai/refactor/reaction-ui-optimistic
refactor(web): migrate reaction optimistic updates to UI pattern
2026-04-08 15:43:51 +08:00
Naiyuan Qing
17e37ec4db fix(web): address review — shared types and stable optimistic data
- Extract ToggleCommentReactionVars and ToggleIssueReactionVars shared
  types so mutation definitions and useMutationState consumers stay in
  sync without as-casts on inline types
- Replace new Date().toISOString() with empty string in optimistic
  reaction objects to avoid unstable references in useMemo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:41:16 +08:00
Naiyuan Qing
060afc848c refactor(web): migrate reaction optimistic updates from cache to UI pattern
Replace cache-level optimistic updates (onMutate with temp IDs) with
TQ v5's UI-level pattern (useMutationState + render-time derivation)
for both issue-level and comment-level reaction toggles.

The cache-level approach caused a race condition: temp IDs in the cache
couldn't be deduplicated against real IDs from WS events, causing
reaction counts to briefly flash incorrect values (e.g. 0→1→2→1).

The UI pattern keeps the cache clean (always server-confirmed data) and
derives optimistic state at render time from pending mutation variables.
WS events safely update the cache without conflicting with temp data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:35:52 +08:00
Naiyuan Qing
1903b886f6 Merge pull request #494 from multica-ai/fix/inbox-stale-timeline-cache
fix(web): add global WS handlers for per-issue cache invalidation
2026-04-08 15:23:45 +08:00
Naiyuan Qing
240813c605 fix(web): add global WS handlers for per-issue cache invalidation
Per-issue WS events (comments, activities, reactions, subscribers) were
only handled by component-level useWSEvent hooks that unsubscribe on
unmount. With staleTime: Infinity, this left timeline/reactions/subscribers
caches silently stale — reopening an issue served cached data without
refetching, causing missing comments in inbox and issue detail views.

Add global fallback handlers in useRealtimeSync that invalidateQueries
for the affected issue on every per-issue WS event, ensuring caches are
marked stale even when IssueDetail is unmounted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:21:14 +08:00
LinYushen
7d74b1f0b9 Merge pull request #495 from multica-ai/revert-477-feat/structured-ticket-search
Revert "feat(issues): add structured ticket search"
2026-04-08 15:15:25 +08:00
LinYushen
39ca8ed9e8 Revert "feat(issues): add structured ticket search" 2026-04-08 15:15:08 +08:00
LinYushen
3c08395741 Merge pull request #477 from pseudoyu/feat/structured-ticket-search
feat(issues): add structured ticket search
2026-04-08 15:02:43 +08:00
LinYushen
ec934f3a8b fix(web): add load-more pagination for Done column on issue board (#492)
* fix(web): add load-more pagination for Done column on issue board

The Done column was capped at 50 issues with no way to load more.
Track doneTotal in the TQ cache and add a useLoadMoreDoneIssues hook
that fetches the next page and merges it into the unified issue cache.
The Done column now shows a "Load more" button when there are
additional items.

- shared/types/api.ts: add doneTotal to ListIssuesResponse
- core/issues/queries.ts: store doneTotal from the done-status response
- core/issues/mutations.ts: add useLoadMoreDoneIssues hook, update
  create/delete mutations to maintain doneTotal
- core/issues/ws-updaters.ts: maintain doneTotal on WS events
- features/issues/components/board-column.tsx: accept optional footer
- features/issues/components/board-view.tsx: render Load more button
  in Done column

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

* fix(web): address review issues in done-column load-more

1. Fix total over-counting: loadMore no longer inflates total since
   the initial query already includes all done issues in total count.
2. Fix onIssueUpdated: maintain doneTotal when issue status changes
   to/from done via WS events.
3. Make doneTotal optional in ListIssuesResponse since it's a
   frontend-only field not returned by the backend API. All reads
   now use ?? 0 fallback.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:58:51 +08:00
Naiyuan Qing
25cf64588d feat(issues): add attachment section with image grid and file cards
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:55:56 +08:00
Naiyuan Qing
301a4a3882 feat(editor): add drag-and-drop visual overlay and file type validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:50:29 +08:00
Naiyuan Qing
102b19d948 feat(upload): add file type whitelist aligned with Agent readability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:44:33 +08:00
zerone0x
a7afd4b959 feat: wire allowedDevOrigins from CORS_ALLOWED_ORIGINS for non-localhost dev access (#355)
* feat: add allowedDevOrigins to Next.js config for non-localhost dev access

Wire CORS_ALLOWED_ORIGINS env var into Next.js allowedDevOrigins config
so that cross-origin HMR/webpack requests from Tailscale or other
non-localhost IPs are not blocked during development.

Fixes #317

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: keep port in allowedDevOrigins

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 14:39:01 +08:00
Bohan Jiang
8403c97688 Merge pull request #482 from multica-ai/agent/j/674c6839
feat(usage): add per-task token usage tracking
2026-04-08 14:16:59 +08:00
LinYushen
7df5750979 fix(daemon): update existing worktree to latest remote on reuse (#489)
* fix(daemon): update existing worktree to latest remote on reuse

When an agent receives a new task on the same issue, the execution
environment is reused and the repo worktree already exists on disk.
Previously, `multica repo checkout` would fail because `git worktree add`
cannot create a path that already exists — so the agent worked on stale
code from the prior task.

Now `CreateWorktree` detects existing worktrees and updates them:
fetch origin, reset working tree, then checkout a new branch from the
latest remote default branch. The previous task's branch is preserved.

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

* fix(daemon): propagate actual branch name and use correct ref in worktree reuse

- Return (string, error) from updateExistingWorktree so collision-retried
  branch name propagates to WorktreeResult
- Use baseRef directly instead of origin/baseRef — bare clone refspec maps
  remote branches to local refs, so remote-tracking refs may not exist
- Remove redundant fetch (worktree shares object store with bare clone)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:13:44 +08:00
Naiyuan Qing
990cc8b3ae Merge pull request #488 from multica-ai/fix/ws-self-event-idempotent
fix(web): replace WS self-event filtering with idempotent cache updates
2026-04-08 14:00:18 +08:00
Naiyuan Qing
7ee2450297 Merge pull request #487 from multica-ai/NevilleQingNY/readonly-markdown
perf(editor): replace readonly Tiptap instances with react-markdown
2026-04-08 13:57:46 +08:00
Naiyuan Qing
d58f6cdb33 fix(web): replace actor_id self-event filtering with idempotent cache updates
actor_id identifies the user, not the browser tab. Filtering WS events
by actor_id broke multi-tab sync — other tabs of the same user would
silently miss updates. Instead, make all WS cache handlers idempotent
(dedup checks on add, no-op on duplicate merge/filter) so mutations and
WS events coexist safely without filtering.

- WSClient: pass actor_id to event handlers for future per-handler use
- use-realtime-sync: remove isSelf() gating from onAny and specific handlers
- useCreateIssue: add .some() dedup guard + onSettled invalidation
- use-issue-reactions: remove payload-level self-filter (dedup already present)
- use-issue-timeline: remove payload-level self-filter on comment:created,
  reaction:added, reaction:removed (dedup already present)
- Clean up useCallback deps that no longer reference userId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:57:24 +08:00
Naiyuan Qing
af156040cb test(issues): add ReadonlyContent mock to issue detail tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:55:25 +08:00
Naiyuan Qing
5e770b2e2f fix(editor): align IssueMentionCard styling and behavior with Tiptap
- Remove align-middle from IssueMentionCard (alignment is container's job)
- Add inline align-middle wrapper span in ReadonlyContent for vertical alignment
- Add img component with max-width constraint to prevent overflow
- Issue mention clicks open in new tab (matches Tiptap behavior)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:47:03 +08:00
Naiyuan Qing
92e76dea81 refactor(issues): use ReadonlyContent for comment readonly display
Replace ContentEditor editable={false} with lightweight ReadonlyContent
in comment cards. Each comment no longer creates a full ProseMirror
instance for readonly display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:47:03 +08:00
Naiyuan Qing
4df32a853b feat(editor): add ReadonlyContent component for lightweight markdown display
- Add del selector to strikethrough CSS for react-markdown compatibility
- Create ReadonlyContent using react-markdown + lowlight + content-editor.css
- Export from editor module index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:47:03 +08:00
Jiang Bohan
fa0c0fe747 fix(usage): address review feedback — independent usage reporting + all providers
1. Separate ReportTaskUsage endpoint (POST /api/daemon/tasks/{id}/usage)
   so usage is captured independently of complete/fail — fixes usage loss
   for failed/blocked tasks.

2. Add usage tracking for all four providers:
   - Claude: already done (stream-json message.usage)
   - OpenCode: extract from step_finish.part.tokens
   - OpenClaw: extract from step_end.data token fields
   - Codex: extract from turn/completed and task_complete usage fields

3. Remove usage from CompleteTask payload — all usage goes through the
   dedicated endpoint now.
2026-04-08 13:23:54 +08:00
Jiang Bohan
8a8d3ea20e feat(usage): add per-task token usage tracking
Extract token usage from Claude Code's stream-json output in real-time
during task execution, replacing the inaccurate global JSONL log scanner.

- New `task_usage` table: tracks (task_id, provider, model) level usage
- Agent SDK: parse `message.usage` from assistant messages, accumulate
  per-model and return in Result
- Daemon: convert agent usage to entries, send with CompleteTask
- Server: store usage on task completion, expose workspace-level
  aggregation APIs (GET /api/usage/daily, GET /api/usage/summary)
2026-04-08 13:08:15 +08:00
Jiayuan Zhang
88c2f4ddc4 Merge pull request #479 from multica-ai/fix/cli-web-shared-login-state
fix(auth): persist browser session during CLI login flow
2026-04-08 12:50:53 +08:00
Bohan Jiang
98af9f442c Merge pull request #471 from multica-ai/agent/j/959392dd
feat: support multiple agents running on same issue
2026-04-08 12:45:56 +08:00
pseudoyu
34c39b765e feat(issues): add structured ticket search 2026-04-08 11:30:53 +08:00
Naiyuan Qing
efe131591f Merge pull request #472 from multica-ai/feat/tanstack-query-migration
feat(web): migrate server state from Zustand to TanStack Query (Phase 0-4)
2026-04-08 10:46:35 +08:00
Naiyuan Qing
104bbbef41 fix(web): prevent useWorkspaceId crash in AppSidebar (re-apply after merge revert)
AppSidebar renders before workspace hydrates. useWorkspaceId() throws
when workspace is null. Fix: read workspace?.id directly from store,
use enabled guard on inbox query. This fix was in commit 030627c but
got reverted by subsequent merge with main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:44:56 +08:00
Naiyuan Qing
eed8e36a69 fix(test): update mockListIssues for two-phase fetch (open_only + closed)
issueListOptions now makes 2 api.listIssues calls (open_only + closed page).
Tests that mock the response must return data only for the open_only call,
otherwise issues appear twice in the merged result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:41:29 +08:00
Naiyuan Qing
8cf78b7a47 Merge remote-tracking branch 'origin/main' into feat/tanstack-query-migration
# Conflicts:
#	apps/web/app/(dashboard)/agents/page.tsx
2026-04-08 10:35:28 +08:00
Naiyuan Qing
862b85e064 fix(web): DnD local-state overlay, onSettled list invalidation, WS self-event filter
- Board DnD: use local pendingMove state for instant card placement,
  bypassing TQ's async setQueryData notification delay
- useUpdateIssue: add list invalidation to onSettled (was only detail)
- use-realtime-sync: add isSelf check to specific issue WS handlers
  (prevents redundant cache writes for own mutations)
- Clean up debug console.logs from board-view, issues-page, mutations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:25:35 +08:00
Jiayuan Zhang
857ec7d4d4 fix(auth): persist browser session during CLI login flow
When authenticating via CLI, the login page called api.verifyCode()
directly and redirected to the CLI callback without saving the JWT
to localStorage or setting the logged-in cookie. This meant the
browser had no session after CLI login, forcing users to log in
again when visiting multica.ai.

Now the token is saved to localStorage and the cookie is set before
redirecting to the CLI callback, so both CLI and web app share the
same authentication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:22:19 +08:00
devv-eve
7c79611309 refactor: remove agent triggers config field (#469)
* refactor: remove agent triggers config field

Remove the triggers field from agent configuration. The on_assign,
on_comment, and on_mention behaviors are now always enabled (hardcoded),
as decided in the Agentflow design discussion (MUL-372).

Changes:
- Database: migration 032 drops triggers column from agent table
- Backend: remove triggers from create/update agent APIs and response
- Backend: simplify trigger-checking logic to always-enabled
- Frontend: remove TriggersTab UI and AgentTrigger types
- Tests: remove trigger config unit tests (no longer configurable)

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

* refactor: also remove agent tools config field

Remove the tools field from agent configuration alongside triggers.
The tools field was a placeholder — stored in the DB and shown in the
UI but never passed to the daemon or used at runtime.

- Database: migration 032 now also drops tools column
- Backend: remove tools from create/update agent APIs and response
- Frontend: remove ToolsTab UI, AgentTool type, and tools tab
- Update landing page copy

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

* fix(test): remove tools/triggers columns from test fixtures

The test fixtures still referenced the dropped tools and triggers
columns when inserting agent rows, causing CI failures.

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

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:02:28 +08:00
Naiyuan Qing
99dad49052 fix(core): add onSettled invalidation to all optimistic mutations + enable refetchOnReconnect
P0: Add onSettled: invalidateQueries to 10 mutations that had onMutate
optimistic updates but no server confirmation. With staleTime: Infinity,
missing onSettled means cache could permanently drift from server state.

Mutations fixed:
- useDeleteIssue, useBatchDeleteIssues (issue list)
- useUpdateComment, useDeleteComment, useToggleCommentReaction (timeline)
- useToggleIssueReaction (reactions)
- useToggleIssueSubscriber (subscribers)
- useMarkInboxRead, useArchiveInbox, useMarkAllInboxRead (inbox)

P2: Change refetchOnReconnect from false to true as safety net
for HTTP reconnection before WS reconnection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:07:37 +08:00
Naiyuan Qing
6296629831 fix: restore TQ consumer migrations lost during merge with main
The merge with origin/main (fe9479d6) silently reverted all consumer-side
migrations, leaving core/ as dead code. Restored all 39 files from
pre-merge commit 6032b5df, plus main's trigger.config null fix for
agents page.

Verified: 59 @core/ imports across features/ and app/, all stores
gutted/deleted, realtime sync uses queryClient not Zustand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:59:09 +08:00
Naiyuan Qing
7ed565da6b docs: update CLAUDE.md for TanStack Query architecture + restore @core alias
- Add core/ layer documentation (queries, mutations, WS updaters)
- Rewrite State Management section: TQ for server state, Zustand for client-only
- Update features table: reflect gutted stores (issues, inbox, workspace)
- Add @core/* import alias examples
- Update Data Flow diagram to include TQ layer
- Restore @core/* path alias in tsconfig + vitest (lost during merge)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:49:23 +08:00
Naiyuan Qing
030627c8c5 fix(web): prevent useWorkspaceId crash in AppSidebar before workspace hydration
AppSidebar renders outside the workspace guard in dashboard layout.
On first login, workspace hasn't hydrated yet → useWorkspaceId() throws.
Fix: read workspace?.id directly from store, use enabled guard on inbox query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:39:32 +08:00
Naiyuan Qing
fe9479d6fc Merge remote-tracking branch 'origin/main' into feat/tanstack-query-migration
# Conflicts:
#	apps/web/features/issues/components/batch-action-toolbar.tsx
#	apps/web/features/issues/components/issues-page.tsx
#	apps/web/features/issues/store.ts
2026-04-07 18:39:13 +08:00
Jiang Bohan
b94108768e feat: support multiple agents running concurrently on the same issue
- Relax ClaimAgentTask SQL constraint from per-issue to per-(issue, agent)
  serialization, allowing different agents to run in parallel on the same issue
- Update GetActiveTaskForIssue API to return all active tasks (array) instead of
  just the first one
- Refactor AgentLiveCard to render one card per active task, routing WebSocket
  messages by task_id for independent timelines
- Fix shouldEnqueueOnComment to use per-agent dedup so a mentioned agent's
  pending task doesn't block the assigned agent's on_comment trigger

Closes MUL-160
2026-04-07 18:19:57 +08:00
Naiyuan Qing
348133b63d merge: resolve conflicts with main (open_only pagination)
- Resolve issues/store.ts: keep client-only store, port pagination
  strategy (open_only + closed page) to core/issues/queries.ts
- Resolve issues-page.tsx, batch-action-toolbar.tsx: keep TQ mutations
- Auto-merge agents/page.tsx trigger null fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:08:35 +08:00
Naiyuan Qing
6032b5dfcb fix: mention closure, onSettled invalidation, cleanup singleton
- Fix Tiptap mention: pass QueryClient via closure from ContentEditor
  instead of getQueryClient() singleton (resolves @mention empty list)
- Add onSettled invalidation to useUpdateIssue (prevents cache drift
  with staleTime: Infinity + self-event WS filter)
- Add cache shape comment to issueListOptions (select transforms
  ListIssuesResponse → Issue[], but cache stores raw response)
- Memoize sidebar inbox dedup computation
- Remove dead getQueryClient/setQueryClient singleton + window property
- Remove ActorSync component and _members/_agents Zustand mirror
  (superseded by closure approach)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:53:49 +08:00
Bohan Jiang
23198f3c26 Merge pull request #461 from multica-ai/agent/j/70455bdb
fix(daemon): correct duplicate sub-step lettering in workflow instructions
2026-04-07 17:29:46 +08:00
Naiyuan Qing
e40341ab73 feat(core): migrate workspace + runtimes to TanStack Query (Phase 3+4)
- Create core/workspace/ with queries (members, agents, skills, list) and mutations
- Create core/runtimes/ with queries
- Migrate 11 consumer files from useWorkspaceStore.members/agents/skills to useQuery
- Replace all WS refreshMap entries with qc.invalidateQueries
- Simplify workspace store: delete members/agents/skills fields + refresh methods,
  hydrateWorkspace becomes synchronous (TQ auto-fetches on component mount)
- Delete useRuntimeStore (no consumers left), runtimes-page uses local useState + TQ
- Remove workspace→runtime cross-store dependency
- Clean up dead test helper mocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:19:52 +08:00
Bohan Jiang
c695de5314 Merge pull request #468 from multica-ai/agent/j/272bc2a3
docs(web): add v0.1.8 changelog entry
2026-04-07 17:07:02 +08:00
Jiang Bohan
d6b59aade6 docs(web): add v0.1.8 changelog entry for 2026-04-07 2026-04-07 17:03:41 +08:00
Naiyuan Qing
1d812bd446 feat(core/inbox): migrate inbox to TanStack Query (Phase 2)
- Create core/inbox/ with queries, mutations, ws-updaters
- Migrate inbox page: useQuery + mutation hooks replace useInboxStore + api.*
- Migrate sidebar unread badge to read from TQ cache
- Delete useInboxStore (127 lines) — inbox has no client-only state
- Remove inbox deps from workspace store (hydrate + switch)
- Fix WS sync: use useQueryClient() instead of getQueryClient() singleton
  to ensure WS handlers write to the same QueryClient instance that
  components read from (singleton is unreliable under Next.js HMR)
- Add onInboxIssueStatusChanged for issue status sync in inbox items

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:56:47 +08:00
devv-eve
abcc7bf3cd feat(issues): load all open issues without limit, paginate closed (#459)
- Add ListOpenIssues SQL query (excludes done/cancelled, no LIMIT)
- Add CountIssues SQL query for true total count
- Backend: support open_only=true param, fix total to return real count
- Frontend: two-phase fetch in issue store (all open + first 50 closed)
- Add fetchMoreClosed action for paginated closed issue loading
- Replace all hardcoded limit:200 with store.fetch() calls

Resolves MUL-369

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:59:03 -07:00
Naiyuan Qing
06fa65d4b5 test(issues): clean up dead useIssueStore mocks from tests
Remove mock issues[] and server state fields from useIssueStore mocks
since the store now only holds activeIssueId. Data flows through
TanStack Query (mockListIssues) not the store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:52:15 +08:00
Bohan Jiang
9d1570b301 Merge pull request #465 from multica-ai/agent/j/ffea36be
fix(auth): move Google callback to correct route path
2026-04-07 15:48:30 +08:00
Jiang Bohan
7f2ea9857d fix(auth): move Google callback page to correct route path
The callback page was placed under app/(auth)/callback/ — a Next.js
route group — which mapped to /callback instead of /auth/callback.
Move it to app/auth/callback/ so the URL matches the Google OAuth
redirect URI.
2026-04-07 15:47:44 +08:00
Naiyuan Qing
1ad057fb0f refactor(issues): migrate all consumers to TanStack Query (Phase 1, Commits 5-10)
- Migrate issue-detail.tsx: useQuery for issue data, useUpdateIssue/useDeleteIssue
- Migrate issues-page.tsx, my-issues-page.tsx, board-card.tsx: useQuery for list
- Migrate batch-action-toolbar.tsx, create-issue.tsx: mutation hooks
- Migrate edge consumers: mention-suggestion, mention-view, agents page, issue-mention-card
- Remove Zustand writes from WS sync (TQ cache is now sole source of truth)
- Remove useIssueStore.fetch() dependency from workspace store
- Gut useIssueStore to client-only: { activeIssueId, setActiveIssue }
- Update test wrappers with QueryClientProvider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:46:08 +08:00
Bohan Jiang
b85c068e83 Merge pull request #464 from multica-ai/agent/j/272bc2a3
docs(web): add v0.1.7 changelog entry
2026-04-07 15:37:23 +08:00
Jiang Bohan
30cda933bc docs(web): add v0.1.7 changelog entry for 2026-04-05 2026-04-07 15:36:32 +08:00
Jiang Bohan
b5537077bc Merge branch 'main' of https://github.com/multica-ai/multica into agent/j/272bc2a3 2026-04-07 15:35:38 +08:00
Bohan Jiang
638033c9ff Merge pull request #462 from multica-ai/agent/j/ffea36be
feat(auth): add Google OAuth login
2026-04-07 15:32:08 +08:00
Naiyuan Qing
7560f7be85 feat(core/issues): add TanStack Query layer and rewrite hooks (Phase 1, Commits 1-4)
- Add getQueryClient() singleton for non-React contexts (WS handlers, Zustand)
- Create issue query key factory + 5 queryOptions
- Create 11 mutation hooks with optimistic updates and rollback
- Create WS cache updaters + dual-write in use-realtime-sync
- Rewrite useIssueTimeline, useIssueReactions, useIssueSubscribers to TQ
  (return types unchanged, consumers unaffected)
- Add QueryClientProvider wrapper to issue detail tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:30:42 +08:00
Bohan Jiang
b84104b421 Merge pull request #463 from multica-ai/fix/agent-trigger-config-nullable-type
fix(types): make AgentTrigger.config nullable
2026-04-07 15:27:11 +08:00
Jiang Bohan
0c92fb2674 fix(types): make AgentTrigger.config nullable to match API reality
The API can return `config: null` for non-scheduled triggers, but the
type was `Record<string, unknown>` which doesn't reflect this. Update
to `Record<string, unknown> | null` so TypeScript catches unsafe access
at compile time.

Follow-up to #415.
2026-04-07 15:25:29 +08:00
Jiang Bohan
14fe8e9df9 feat(auth): add Google OAuth login
Support Google login that links to existing accounts by email.
When a user who registered via email OTP signs in with Google using
the same email, they are linked to the same account.

Backend:
- Add POST /auth/google endpoint that exchanges Google auth code for
  tokens, fetches user profile, and calls findOrCreateUser()
- Updates user name and avatar from Google profile on first Google login

Frontend:
- Add "Continue with Google" button on login page (shown when
  NEXT_PUBLIC_GOOGLE_CLIENT_ID is configured)
- Add /auth/callback page to handle Google OAuth redirect
- Add loginWithGoogle to auth store and API client
2026-04-07 15:25:26 +08:00
Bohan Jiang
f9c0fcba24 Merge pull request #415 from cocovs/codex/fix-agent-trigger-null-config-crash
fix(web): prevent Agents trigger crash when config is null
2026-04-07 15:24:15 +08:00
Jiang Bohan
47917825d1 fix(daemon): correct duplicate sub-step lettering in workflow instructions
When repos are present, sub-steps c/d/e/f are now distinct instead of
having two 'c' steps. Each branch (with/without repos) now has its own
complete set of correctly lettered sub-steps.
2026-04-07 15:22:02 +08:00
Bohan Jiang
eab5f8e7e8 Merge pull request #457 from multica-ai/agent/j/4420d1bf
fix(daemon): ensure multica CLI is on PATH in agent task environment
2026-04-07 15:03:56 +08:00
Jiang Bohan
9495179923 fix(daemon): ensure multica CLI is on PATH in agent task environment
Prepend the directory of the running multica binary to PATH in the
agent's environment variables. This fixes the issue where isolated
runtimes (e.g. Codex sandbox) cannot find the multica CLI, causing
agent tasks to fail immediately with "command not found: multica".

Closes #451
2026-04-07 15:01:48 +08:00
Bohan Jiang
f16b36fbc8 Merge pull request #456 from multica-ai/agent/j/25583cc6
feat(agent): add OpenClaw runtime support
2026-04-07 14:53:53 +08:00
Jiang Bohan
dd2ce90b1d fix(agent): address openclaw review feedback
- Remove duplicate extractOCToolOutput, reuse extractToolOutput from opencode.go
- Rename extractEventText → openclawExtractText to avoid package-level name collisions
- Add clarifying comments for error status stickiness and result event behavior
- Remove redundant extractOCToolOutput tests (already covered by opencode tests)
2026-04-07 14:52:54 +08:00
Bohan Jiang
88b87e2fa6 Merge pull request #455 from multica-ai/agent/j/653cfab4
fix(triggers): remove assignee skip in on_mention trigger
2026-04-07 14:49:45 +08:00
Naiyuan Qing
2be9f6cd2f feat(web): add TanStack Query infrastructure (Phase 0)
- Install @tanstack/react-query v5 + devtools
- Create core/query-client.ts with WS-optimized defaults (staleTime: Infinity)
- Create QueryProvider and wire into root layout
- Add @core/* path alias to tsconfig + vitest
- Add useWorkspaceId() bridge hook for query key scoping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:43:51 +08:00
Jiang Bohan
5cf4ba803d feat(agent): add OpenClaw runtime support
Add OpenClaw as a fourth supported agent runtime alongside Claude Code,
Codex, and OpenCode. OpenClaw CLI (`openclaw agent -p ... --output-format
stream-json`) is integrated via the same Backend interface pattern.

Changes:
- Add openclawBackend in server/pkg/agent/openclaw.go with NDJSON
  event stream parsing (text, thinking, tool_call, error, step, result)
- Register "openclaw" in the agent factory (agent.go)
- Add MULTICA_OPENCLAW_PATH / MULTICA_OPENCLAW_MODEL env var detection
  in daemon config
- Include "openclaw" in AGENTS.md config injection alongside codex/opencode
- Add comprehensive unit tests for all event handlers and processEvents
2026-04-07 14:40:51 +08:00
Jiang Bohan
cfb0365cb3 fix(triggers): remove assignee skip in enqueueMentionedAgentTasks
The assignee check in enqueueMentionedAgentTasks silently skipped
explicit @mentions when the target agent was the issue assignee in
a non-terminal status. This broke the review-rejection-retry loop:
when a reviewer rejected a PR and @mentioned the developer agent,
the mention was skipped because the developer was the assignee.

The downstream HasPendingTaskForIssueAndAgent check already prevents
duplicate queued tasks, making the assignee skip redundant. Removing
it ensures explicit @mentions always fire regardless of assignee status.

Closes #431
2026-04-07 14:36:08 +08:00
devv-eve
81d430d870 Merge pull request #445 from sunjie21/main
fix(auth): extend JWT and CloudFront cookie expiration from 72h to 30 days
2026-04-06 23:34:15 -07:00
Bohan Jiang
96d81f9836 Merge pull request #454 from multica-ai/agent/j/ea6693b0
fix(daemon): add missing CLI commands to agent instructions
2026-04-07 14:23:24 +08:00
Naiyuan Qing
5fe1ec806d docs: add TanStack Query migration plan
Phase 0-5 plan for migrating server state from Zustand to TanStack Query,
extracting headless business logic to core/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:20:43 +08:00
Bohan Jiang
2f63714dba Merge pull request #410 from jtsang4/main
fix(build): include migrate binary in make build
2026-04-07 14:18:57 +08:00
Bohan Jiang
4cf18e122d Merge pull request #413 from cocovs/codex/fix-daemon-pid-minus1
fix(cli): preserve daemon pid before releasing child process
2026-04-07 14:18:12 +08:00
Jiang Bohan
02a7598906 fix(daemon): add missing CLI commands to agent instructions
Add 5 missing commands to buildMetaSkillContent() so agents can
discover them:

Read:
- workspace members — query member IDs for mentions
- repo checkout — listed in command reference, not just prose

Write:
- issue create — create sub-issues and new tasks
- issue assign — assign/unassign issues
- issue comment delete — remove erroneous comments
2026-04-07 14:13:26 +08:00
Junlong
0263ecce9e Docs: fix self hosting local deploy protocol (#433)
* fix: skip Docker check in ensure-postgres.sh when remote DATABASE_URL is set

When DATABASE_URL points to a non-localhost host, the script now skips
all Docker operations and only verifies remote DB connectivity via
pg_isready directly.

* fix: honor DATABASE_URL for remote postgres preflight

* fix(make): clarify stop output for remote database

* docs: add local deployment protocol guidance to SELF_HOSTING.md

Clarify that local deployments without TLS should use http:// and ws://
instead of https:// and wss://.

---------

Co-authored-by: Junlong Liu <junlong.liu@shopee.com>
2026-04-07 14:08:06 +08:00
yihong
d450b3d454 fix: run make test command (#449)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2026-04-07 13:52:52 +08:00
Bohan Jiang
f1140222a1 Merge pull request #441 from quake/docs/cli-install-guide
docs: add CLI_INSTALL.md for agent-driven setup and update READMEs
2026-04-07 13:50:06 +08:00
诺墨
66067a267a fix(makefile): binary build missing for migration (#447)
Signed-off-by: 诺墨 <normal@normalcoder.com>
2026-04-07 13:47:05 +08:00
Bohan Jiang
76c6b41033 Merge pull request #453 from multica-ai/agent/j/4c35cc35
docs: add PR template
2026-04-07 13:32:38 +08:00
Jiang Bohan
29507a2e3a docs: add AI prompt field to PR template
Encourage contributors to share the prompt they used when AI tools
were involved, helping reviewers understand intent and enabling
knowledge sharing across the community.
2026-04-07 13:30:54 +08:00
Jiang Bohan
ceec6d3795 docs: add PR template
Adds a structured PR template requiring change description, motivation,
type classification, test plan, and an optional AI disclosure field.
Part of the Phase 1 community management improvements (MUL-320).
2026-04-07 13:23:17 +08:00
Naiyuan Qing
08ba74b399 Merge pull request #309 from multica-ai/agent/lambda/83f444ab
fix(web): navigate to /issues when switching workspaces
2026-04-07 10:30:14 +08:00
Naiyuan Qing
ed7a288946 fix(web): prevent 404 on workspace switch and downgrade 404 log level
- Skip issue refetch when store is cleared during workspace switch by
  tracking which issue was already loaded (loadedIdRef pattern)
- Downgrade 404 responses from logger.error to logger.warn in ApiClient
  since resource-not-found is a normal business response, not a bug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:26:53 +08:00
Naiyuan Qing
a26f9e965b Merge pull request #448 from multica-ai/refactor/agent-live-card-sticky
refactor(web): redesign agent live card sticky behavior
2026-04-07 09:55:08 +08:00
Naiyuan Qing
6574d68d2b refactor(web): redesign agent live card — always sticky with manual toggle
Replace the oscillation-prone IntersectionObserver/sentinel pattern with
a simpler always-sticky collapsible card. The card defaults to collapsed
(mini bar) and users toggle it manually. Outer scroll auto-collapses the
timeline to stay out of the way, with scroll-chaining prevention via
overscroll-behavior-y: contain.

Key changes:
- Remove sentinel, IntersectionObserver, and bidirectional isStuck state
- Always sticky at top-4 with unified info color scheme
- Manual toggle via clickable header with grid-rows animation
- Auto-collapse on outer scroll (one-way, prevents oscillation)
- Consolidate three task-end handlers into single handleTaskEnd
- Add hover interaction (muted-foreground → foreground)
- Add aria-expanded for accessibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:47:02 +08:00
sunjie21
3bf094ebf7 fix(auth): extend JWT and CloudFront cookie expiration from 72h to 30 days
Reduces login frequency for users by increasing token lifetime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:48:31 +08:00
quake
72da372eba docs: add CLI_INSTALL.md for agent-driven setup and update READMEs
Add a structured installation guide (CLI_INSTALL.md) designed for AI agents
to fetch and execute step-by-step: install CLI, authenticate, and start the
daemon. Update README and README.zh-CN CLI sections with an agent-friendly
paste option alongside the existing manual instructions.

Also fix brew formula name in CLI_AND_DAEMON.md (multica-cli → multica) to
match .goreleaser.yml.
2026-04-06 21:15:30 +09:00
Jiayuan Zhang
5fba76f010 fix(web): remember last selected workspace after re-login (#435)
Stop clearing multica_workspace_id from localStorage on logout so it
persists as a preference hint. On fresh login, pass the stored ID to
hydrateWorkspace so the user returns to their last workspace instead
of always landing on the first one.
2026-04-06 01:18:44 +08:00
LinYushen
09565bc40f Merge pull request #426 from multica-ai/fix/attachment-upload-linking
fix(attachment): use UUIDv7 as S3 key and link attachments on issue/comment creation
2026-04-05 08:04:11 +08:00
yushen
4036d64996 fix(attachment): use UUIDv7 as S3 key and link attachments on issue/comment creation
- Use google/uuid NewV7() for attachment ID and S3 file key instead of
  random hex, so the S3 object name matches the attachment record ID
- Add LinkAttachmentsToIssue query to associate orphaned attachments
  with a newly created issue
- Pass attachment_ids in CreateIssue request so uploads during issue
  creation (before the issue exists) get linked after commit
- Collect and pass attachment IDs in comment-input and reply-input
  so comment creation properly links uploaded files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:55:17 +08:00
LinYushen
5b0a537302 Merge pull request #425 from multica-ai/agent/eve/90e2273d
refactor(cli): improve help UX with examples and arg validation
2026-04-05 07:05:23 +08:00
yushen
0d9d4e6b69 merge: resolve conflicts with origin/main in help.go
Keep branch additions (errSilent, exactArgs, examples template blocks)
that were added in the CLI help UX improvement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:03:03 +08:00
yushen
4c0dbbf1c8 refactor(cli): improve help UX — add examples support, show help on arg errors
- Add EXAMPLES section to leaf and sub help templates (gh CLI style)
- Add example to attachment download command
- Simplify attachment download description
- Show help output when required args are missing (error first, then help)
- Replace cobra.ExactArgs with custom exactArgs that prints help on failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:00:19 +08:00
devv-eve
52a9a6ae5f refactor(cli): overhaul help output to match gh CLI style (#423)
* refactor(cli): overhaul help output to match gh CLI style

- Add gh-style grouped help with CORE/RUNTIME/ADDITIONAL COMMANDS sections
- Use UPPERCASE section headers (USAGE, FLAGS, EXAMPLES, LEARN MORE)
- Format commands as "name:  description" with automatic alignment
- Add ENVIRONMENT VARIABLES and EXAMPLES sections to root help
- Apply consistent templates to root, subcommand, and leaf commands
- Update descriptions from "Manage X" to "Work with X" for gh parity

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

* fix(execenv): add explicit instruction for agents to always use multica CLI

Agents were using curl/wget to access Multica attachment URLs directly,
which fails due to authentication. Add a prominent "Important" section
to the generated CLAUDE.md template that explicitly prohibits direct
HTTP access and instructs agents to escalate missing CLI functionality
to their workspace owner.

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:30:40 -07:00
Devv
d6a5ba4d5e fix(execenv): add explicit instruction for agents to always use multica CLI
Agents were using curl/wget to access Multica attachment URLs directly,
which fails due to authentication. Add a prominent "Important" section
to the generated CLAUDE.md template that explicitly prohibits direct
HTTP access and instructs agents to escalate missing CLI functionality
to their workspace owner.
2026-04-04 15:27:50 -07:00
Devv
4afef09a03 refactor(cli): overhaul help output to match gh CLI style
- Add gh-style grouped help with CORE/RUNTIME/ADDITIONAL COMMANDS sections
- Use UPPERCASE section headers (USAGE, FLAGS, EXAMPLES, LEARN MORE)
- Format commands as "name:  description" with automatic alignment
- Add ENVIRONMENT VARIABLES and EXAMPLES sections to root help
- Apply consistent templates to root, subcommand, and leaf commands
- Update descriptions from "Manage X" to "Work with X" for gh parity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:10:59 -07:00
Jiayuan Zhang
0771c15a59 fix(trigger): skip parent mention inheritance when reply @mentions only members (#421)
When a reply in a thread explicitly mentions only non-agent entities
(members or issues), do not inherit agent mentions from the parent
comment. This prevents false agent triggers when a user is directing
their reply at other people (e.g. "cc @Someone") rather than requesting
work from agents mentioned in the thread root.

Fixes MUL-324
2026-04-05 04:44:24 +08:00
Jiayuan Zhang
3a96567fc1 fix(web): remove duplicate emoji button on comment card (#419)
* fix(web): remove duplicate emoji button on parent comment card

The parent CommentCard rendered two emoji pickers: one in the header
toolbar (QuickEmojiPicker) and another inside ReactionBar (which has
its own QuickEmojiPicker when hideAddButton is not set). Added
hideAddButton to the parent's ReactionBar, matching the pattern
already used in CommentRow for replies.

* fix(web): show emoji button at bottom for long comments

For short comments, the emoji picker only appears in the top-right
toolbar. For long comments (>500 chars or >8 newlines), the ReactionBar
also shows an add button at the bottom so users don't have to scroll
back up to add reactions.
2026-04-05 04:17:36 +08:00
周阳
9d9e0317c0 fix(web): handle null trigger config in agents page 2026-04-04 22:15:15 +08:00
周阳
5f2ac17129 fix(cli): preserve daemon pid before releasing child process 2026-04-04 21:37:40 +08:00
jtsang4
4df3a52c4e fix(build): include migrate binary in make build 2026-04-04 19:12:17 +08:00
Jiayuan Zhang
9aee403ff9 Merge pull request #405 from multica-ai/agent/lambda/8bf9407f
fix(comment): allow @mention of assigned agent on done/cancelled issues
2026-04-04 13:31:07 +08:00
Jiayuan Zhang
7883fe7bd7 Merge pull request #406 from multica-ai/fix/setup-worktree-env-overwrite
fix: make setup-worktree preserve existing .env.worktree
2026-04-04 13:26:17 +08:00
Jiayuan
cbfb7d58b6 fix: make setup-worktree preserve existing .env.worktree
setup-worktree was using FORCE=1 to unconditionally regenerate
.env.worktree, which overwrites any manual edits (e.g. switching
to the main DB). Now it only generates the file if it doesn't exist.
2026-04-04 13:23:14 +08:00
Jiayuan
2832a06fe3 fix(comment): allow @mention of assigned agent on done/cancelled issues
The assignee was unconditionally skipped in the mention path, assuming
on_comment would handle it. But on_comment is suppressed for terminal
statuses (done/cancelled), so an explicit @mention of the assignee had
no effect. Now only skip the assignee dedup when on_comment will
actually fire (non-terminal status).
2026-04-04 13:14:09 +08:00
Jiayuan Zhang
451715f5a1 fix(web): prevent Archive Agent button text from wrapping to two lines (#404)
Add w-auto class to DropdownMenuContent on agent detail panel, matching
the pattern used by other dropdowns in the codebase. The default
w-(--anchor-width) was constraining the popup to the icon button width.
2026-04-04 12:59:25 +08:00
Jiayuan Zhang
fdf594155c Merge pull request #396 from multica-ai/feat/comment-list-pagination
feat(comments): add pagination to comment list API and CLI
2026-04-04 01:07:22 +08:00
Jiayuan
c39470a53f fix(comments): address code review feedback on pagination
1. Update CLAUDE.md template to document --limit, --offset, --since
   params and guide agents to use pagination when comments are large
2. Add GetJSONWithHeaders to API client; CLI now prints "Showing X of Y
   comments" to stderr when paginating
3. Cap --since without --limit at 500 server-side to prevent unbounded
   result sets
2026-04-04 01:01:48 +08:00
Jiayuan Zhang
e5dfb34a2a Merge pull request #398 from multica-ai/agent/lambda/df68aca8
fix(inbox): archive at issue level instead of event level
2026-04-04 00:30:04 +08:00
Jiayuan
58549975e0 fix(inbox): archive all items for the same issue instead of just one
The inbox UI deduplicates items by issue_id (showing only the latest
notification per issue). Previously, clicking archive only archived the
single visible item, so older items for the same issue would reappear.

Now archiving operates at the issue level — both the backend and frontend
archive all inbox items sharing the same issue_id.
2026-04-04 00:18:14 +08:00
Jiayuan
0bbc6bc1c5 feat(comments): add pagination support to comment list API and CLI
Add --limit, --offset, and --since flags to `multica issue comment list`
to prevent context window overflow when issues have many comments.

The API endpoint now accepts limit, offset, and since (RFC3339) query
parameters. When paginating, the response includes an X-Total-Count
header with the total number of comments.
2026-04-03 23:53:00 +08:00
Bohan Jiang
beeb8bc107 Merge pull request #361 from JimmyPang02/fix/issue-mention-on-comment
fix: issue mentions should not suppress on_comment trigger
2026-04-03 18:42:16 +08:00
Naiyuan Qing
5548d60dbb Revert "fix(issues): prevent sticky mini bar oscillation with height placeholder"
This reverts commit 9fb25f4543.
2026-04-03 18:35:52 +08:00
Naiyuan Qing
9fb25f4543 fix(issues): prevent sticky mini bar oscillation with height placeholder
When the agent live card collapses to sticky mode, its height drops from
~320px to ~40px. This layout shift caused content below to jump up,
re-triggering IntersectionObserver and creating an infinite loop.

Fix: capture the card's expanded height before collapsing, then set
minHeight on a wrapper div to preserve the space. Content below stays
put, sentinel stays out of view, no oscillation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:30:09 +08:00
Bohan Jiang
33140d4c5a docs(web): add v0.1.6 changelog entry (#391)
* docs(web): add v0.1.5 changelog entry for 2026-04-02

* docs(web): add v0.1.6 changelog entry for 2026-04-03
2026-04-03 16:39:56 +08:00
Jiang Bohan
2787bd60be docs(web): add v0.1.6 changelog entry for 2026-04-03 2026-04-03 16:38:04 +08:00
Jiang Bohan
e879d82e7d Merge branch 'main' of https://github.com/multica-ai/multica into agent/j/272bc2a3 2026-04-03 16:37:12 +08:00
Jiayuan Zhang
9b8cc0870b Merge pull request #388 from multica-ai/agent/lambda/1286014b
feat(cli): add agent, skill, and runtime management commands
2026-04-03 16:14:37 +08:00
Naiyuan Qing
ce40b66c60 Merge pull request #390 from multica-ai/feat/agent-live-card-sticky-minibar
feat(issues): sticky mini bar for agent live card + toast icon colors
2026-04-03 16:09:27 +08:00
Naiyuan Qing
56b49cb2a6 feat(issues): use ActorAvatar in agent live card header
Replace hand-rolled Bot icon circle with ActorAvatar component so
agent custom avatars display correctly, consistent with comment cards
and other agent-rendered UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:08:12 +08:00
Naiyuan Qing
4353340ea6 feat(issues): sticky mini bar for agent live card + toast icon colors
Agent live card now uses the sentinel pattern to detect when it scrolls
out of view. When stuck, it collapses to a compact header bar with brand
styling and backdrop blur, with a ChevronUp button to scroll back.
When scrolled back into view, the card seamlessly expands to full view.

Also adds semantic colors to Sonner toast icons (success/info/warning/
error/loading) and fixes icon-to-text alignment in toasts globally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:05:35 +08:00
Jiayuan
91cbf32fd1 fix(cli): address code review feedback for agent/skill/runtime commands
- Add --config flag to skill create/update (accepts JSON string)
- Add --runtime-config flag to agent create/update (accepts JSON string)
- Add --yes flag to skill delete with confirmation prompt
- Improve agent skills set error message (guide users to --skill-ids '')
- Validate --days range (1-365) in runtime usage
- Include last known status in ping/update timeout errors
2026-04-03 16:00:12 +08:00
Jiayuan
10b482fac2 feat(cli): add agent, skill, and runtime management commands
Expand CLI coverage for agent/skill/runtime APIs that previously had no
CLI wrappers despite having fully implemented backend endpoints.

Agent commands (was: only list):
- agent get/create/update/archive/restore/tasks
- agent skills list/set
- agent list --include-archived

Skill commands (new, was: 0% coverage):
- skill list/get/create/update/delete/import
- skill files list/upsert/delete

Runtime commands (new, was: 0% coverage):
- runtime list/usage/activity/ping/update
- ping and update support --wait for polling
2026-04-03 15:41:17 +08:00
Jiayuan Zhang
0024208354 Merge pull request #387 from multica-ai/fix/filter-archived-agents-from-dropdowns
fix(web): filter archived agents from dropdown selectors
2026-04-03 15:41:01 +08:00
Bohan Jiang
32a3a3543d docs(web): add v0.1.5 changelog entry for 2026-04-02 (#386) 2026-04-03 15:40:15 +08:00
Jiayuan
e314badf18 fix(web): filter archived agents from all dropdown selectors
Add `!a.archived_at` check to agent filters in create-issue modal,
issues-header filter panel, issue-detail assignee dropdown, and
issue-detail subscriber list.

assignee-picker and mention-suggestion already filter correctly.
2026-04-03 15:39:27 +08:00
Jiang Bohan
ad0615a08f docs(web): add v0.1.5 changelog entry for 2026-04-02 2026-04-03 15:38:38 +08:00
Bohan Jiang
fc6405e4be fix(trigger): allow on_comment when thread root @mentions assignee agent (#382)
When a member-started thread root @mentions the assignee agent, replies
in that thread should trigger on_comment — the thread is a conversation
with the agent, not a member-to-member chat.

Previously isReplyToMemberThread only checked the reply content for
assignee mentions. Now it also checks the parent (thread root) content.
This fixes a gap where path 1 (on_comment) suppressed the trigger and
path 2 (on_mention) skipped the assignee, leaving no trigger path.
2026-04-03 15:07:39 +08:00
devv-eve
7b610a4013 feat(agents): hide archived agents from default list (#373)
* feat(agents): hide archived agents from default list

Archived agents are now filtered out of the default agent list view.
A toggle button (archive icon) appears when archived agents exist,
allowing users to switch between viewing active and archived agents.
The @mention suggestion list already filters out archived agents.

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

* fix(agents): show "No active agents" when all agents are archived

When there are archived agents but no active ones, the empty state now
shows "No active agents" instead of "No agents yet" to avoid confusion.

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

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:59:36 +08:00
Bohan Jiang
978e81a268 fix(inbox): prevent comment highlight from re-triggering on every timeline change (#374)
The useEffect that scrolls to and highlights a comment had timeline.length
in its dependency array. When a new reply was posted, timeline.length changed,
re-triggering the scroll and highlight animation. Added a ref to track whether
we've already highlighted for the current highlightCommentId so it only fires once.
2026-04-03 13:47:20 +08:00
Bohan Jiang
c9c8230271 feat(trigger): inherit thread-root mentions for reply-triggered agent tasks (#375)
When a top-level comment @mentions an agent (non-assignee), subsequent
replies in the same thread now also trigger that agent via on_mention.
Previously only the current comment's mentions were checked, so replies
without an explicit re-mention would silently skip the agent.

Extends enqueueMentionedAgentTasks to accept the parent comment and
merge its parsed mentions (deduplicated) into the trigger set, reusing
all existing guards (self-trigger, assignee skip, visibility, dedup).

Closes MUL-177
2026-04-03 13:46:07 +08:00
Naiyuan Qing
b84543e634 Merge pull request #371 from multica-ai/refactor/editor-unified-markdown-pipeline
refactor(editor): unify editor with single markdown pipeline
2026-04-03 11:22:58 +08:00
Naiyuan Qing
6c651f4be5 docs(editor): annotate key files with design decisions and pitfalls
Add architecture comments to content-editor.tsx, markdown-paste.ts,
extensions/index.ts, mention-view.tsx, content-editor.css, and
preprocess.ts explaining: why single markdown pipeline, why
data-pm-slice for paste detection, typography benchmarks, mention
card sizing rationale, and what was removed from the old system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:21:54 +08:00
Naiyuan Qing
b5924ffa99 fix(editor): reliable markdown paste and inline code line spacing
Markdown paste: replace heuristic-based detection with a single
deterministic check — only use HTML clipboard path when source is
another ProseMirror editor (identified by data-pm-slice attribute).
All other pastes (VS Code, text editors, terminals, .md files) parse
text/plain as Markdown via @tiptap/markdown.

Inline code: add box-decoration-break: clone and line-height: 2 so
multi-line inline code renders with proper spacing between lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:17:03 +08:00
Naiyuan Qing
30640436c4 refactor(editor): style links with brand color and subtle underline
- Use var(--brand) instead of var(--primary) for link color (blue vs
  near-black)
- Add default underline at 40% opacity, full opacity on hover
- Remove Tailwind HTMLAttributes from Link extensions — let CSS control
  all link styling uniformly
- Mention cards unaffected (a.issue-mention overrides with color: inherit)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:47:19 +08:00
Naiyuan Qing
eb355dbc9c fix(editor): improve mention card sizing and vertical alignment
- Restore card to readable size (py-0.5, px-2, text-xs, rounded-md)
- Add max-w-72 and truncate on title for long issue names
- Move vertical-align: middle to [data-node-view-wrapper] (outermost
  inline element) instead of inner <a> — fixes centering within line
- Always render card style even when issue not in store (fallback shows
  identifier only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:44:04 +08:00
Naiyuan Qing
f34ed091e7 fix(editor): make issue mention card fit within text line height
Reduce card dimensions so it doesn't overflow the paragraph line box:
- Padding: py-0.5 → py-px (4px → 1px vertical)
- Font size: text-sm → text-xs with leading-relaxed
- Alignment: vertical-align: middle via CSS selector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:33:08 +08:00
Naiyuan Qing
d9a6b8c8ed refactor(editor): polish typography and fix mention card alignment
Typography adjustments based on GitHub/Tailwind prose research:
- Heading sizes: h1 22px, h2 18px, h3 15px (was 18/16/14 — h3 was
  indistinguishable from body text)
- Paragraph spacing: 10px (was 8px)
- List indentation: 20px for ul (was 16px)
- Code block margin: 12px (was 8px)
- Blockquote border: 3px (was 2px)

Issue mention card fixes:
- Vertical alignment: align-text-bottom (was align-middle, caused
  upward shift)
- Internal gap: gap-2 (was gap-1.5)
- Horizontal padding: px-2.5 (was px-2)
- External margin: mx-0.5 for breathing room with surrounding text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:29:41 +08:00
Naiyuan Qing
27e58d91af refactor(editor): unify editor into features/editor with single markdown pipeline
Replace three divergent data paths (Marked HTML loading, regex post-processing
saving, separate paste parsing) with one symmetric path through @tiptap/markdown.

Key changes:
- Create features/editor/ module with ContentEditor (unified edit+readonly)
  and TitleEditor, replacing components/common/ editor files
- Load content via contentType: 'markdown' instead of markdownToHtml() hack
- Save content via editor.getMarkdown() directly, no post-processing
- Merge RichTextEditor + ReadonlyEditor into single ContentEditor with
  editable prop
- Extract extensions into separate modules (mention, file-upload,
  markdown-paste, submit-shortcut, code-block-view)
- Extract shared preprocessMentionShortcodes to components/markdown/mentions.ts
- Add copyMarkdown utility for clipboard operations
- Upgrade all @tiptap packages from 3.20.5 to 3.22.1 (lexer isolation fix,
  HTML entity roundtrip fix, table alignment support)
- Delete markdownToHtml.ts, readonly-editor.tsx, and 10 old component files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:28:29 +08:00
Jimmy Peng
6799458807 fix: issue mentions should not suppress on_comment trigger 2026-04-03 01:12:38 +08:00
devv-eve
8eb1caa72b fix(agent): instruct agents to use download_url for attachments (#356)
* fix(agent): instruct agents to use download_url for attachments

Agents were not aware of the signed vs unsigned URL distinction in
attachments, causing failures when trying to read images. Added an
Attachments section to the generated CLAUDE.md/AGENTS.md template that
tells agents to always use `download_url`. Also increased signed URL
expiry from 5 to 30 minutes to better accommodate agent processing time.

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

* feat(cli): add `multica attachment download` command

Adds a dedicated CLI command for downloading attachments by ID. The
command fetches attachment metadata from the API (which returns a fresh
signed URL), downloads the file, and saves it locally. This eliminates
the need for agents to understand signed vs unsigned URLs.

Changes:
- New `multica attachment download <id>` CLI command
- New `GET /api/attachments/{id}` backend endpoint
- `DownloadFile` helper on APIClient
- Updated CLAUDE.md template to document the command

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

* fix(cli): sanitize filename and add download size limit

- Use filepath.Base on attachment filename to prevent path traversal
- Add 100MB size limit to DownloadFile (matches upload limit)
- Include response body in download error messages for debugging

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

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 07:45:42 -07:00
Naiyuan Qing
35b379d688 Merge pull request #354 from multica-ai/refactor/editor-typography-polish
refactor(editor): polish typography for better visual hierarchy
2026-04-02 20:14:08 +08:00
Naiyuan Qing
392a8d7c8c refactor(editor): polish typography for better visual hierarchy
- Heading hierarchy: H1 bumped to 1.125rem with letter-spacing to
  distinguish from H2 (both were 1rem). Margins normalized to 0.5rem
  baseline rhythm.
- List items: increased spacing from 0.125rem to 0.25rem for readability.
  Remove paragraph margins inside list items (Tiptap wraps li content
  in <p> tags which inherited 0.5rem margins).
- Nested lists: bullet style progression (disc → circle → square) and
  numbering progression (decimal → lower-alpha → lower-roman).
- Blockquotes: tighter paragraph spacing inside, nested blockquotes get
  lighter border for depth indication.
- Inline code: border-radius uses semantic --radius-sm token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:10:45 +08:00
Naiyuan Qing
fd744c331e Merge pull request #352 from multica-ai/agent/naiyuan-agent/2a52f202
fix(inbox): parse issue query param on initial page load
2026-04-02 19:32:38 +08:00
Naiyuan Qing
a98f165458 Merge pull request #353 from multica-ai/fix/editor-markdown-rendering
fix(editor): reliable markdown rendering via marked HTML pipeline
2026-04-02 19:26:18 +08:00
Naiyuan Qing
097630c733 fix(editor): reliable markdown rendering via marked HTML pipeline
Replace @tiptap/markdown's beta contentType: "markdown" parser with a
dedicated marked-based HTML pipeline for loading markdown content.

The @tiptap/markdown parser silently drops content in complex documents
(tables, nested lists, mentions). Instead, we now:

1. Pre-convert mention links to <span data-type="mention"> HTML
2. Render markdown to HTML via a dedicated Marked instance with a custom
   renderer that wraps table cell content in <p> tags (required by
   Tiptap's TableCell block+ content spec)
3. Load as HTML — Tiptap's ProseMirror HTML parser handles everything
4. Keep @tiptap/markdown extension only for getMarkdown() serialization

Also adds Table extension support and aligns CSS with the old Markdown
component's minimal mode styling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:24:33 +08:00
Naiyuan Qing
c3cca50f27 fix(inbox): parse issue query param on initial page load
Use useState for selectedKey instead of deriving directly from
useSearchParams(), so the issue ID from ?issue=<id> is reliably
captured on mount. window.history.replaceState() doesn't always
sync back to useSearchParams() in Next.js, causing the detail
panel to show empty when entering via a shared inbox link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:51:14 +08:00
LinYushen
36ba23b3cd fix(test): use auth.JWTSecret() in integration tests instead of hardcoded secret (#349)
The integration tests hardcoded the old default JWT secret while .env
sets a different JWT_SECRET, causing all authenticated requests to fail
with 401. Use auth.JWTSecret() so tests stay in sync with the server.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:21:21 +08:00
Naiyuan Qing
5df444ba00 Merge pull request #347 from multica-ai/agent/naiyuan-agent/00dfb0e6
fix(realtime): prevent full refetch on issue/inbox WS events
2026-04-02 18:17:07 +08:00
Naiyuan Qing
e6a1ff4354 Merge pull request #348 from multica-ai/fix/unify-image-upload-flow
fix(editor): unify image upload flow for paste and button
2026-04-02 18:05:11 +08:00
Naiyuan Qing
7cc4e63e0e fix(editor): unify image upload flow for paste and button
Paste/drop and attachment button previously used separate upload paths.
The button uploaded first then called insertFile (which replaced the
current selection), while paste inserted a blob preview first. This
caused the second image to overwrite the first when both were used.

Now both paths share the same flow via uploadAndInsertFile():
blob preview with uploading animation → background upload → replace URL.

- Extract shared uploadAndInsertFile() function
- Replace insertFile ref method with uploadFile (inserts at doc end)
- Simplify FileUploadButton to onSelect(file) — no more onUpload/onInsert
- Wire onUploadFile in comment edit mode (was missing, upload was no-op)
- Unify image border-radius CSS for both editing and readonly modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:02:28 +08:00
Quake Wang
36db325d50 feat(daemon): add opencode as supported agent provider (#341)
* feat(daemon): add opencode as supported agent provider

Add opencode backend alongside claude and codex. The backend spawns
`opencode run --format json`, parses streaming JSON events (text,
tool_use, error, step_start/finish), and supports --prompt for system
prompts. Includes CLI detection, AGENTS.md runtime config, native skill
discovery via .config/opencode/skills/, and 21 tests covering handlers,
JSON parsing, and integration-level processEvents scenarios.

* chore: add .tool-versions to gitignore
2026-04-02 17:52:07 +08:00
Naiyuan Qing
d751373368 fix(realtime): handle issue/inbox events granularly to prevent full refetch
When viewing an issue in the inbox, WS events like issue:updated and
inbox:new triggered full store refetches, causing unnecessary loading
flashes and redundant API calls. Now these events update the store
in-place using the event payload data instead of refetching everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:36:44 +08:00
LinYushen
09764c5f51 feat(agent): replace hard delete with archive/restore (#346)
* feat(agent): replace hard delete with archive/restore

Replace agent deletion with soft archive pattern. Archived agents
are preserved in the database with all historical references intact
but cannot be assigned, mentioned, or trigger tasks.

Backend:
- Add archived_at/archived_by columns to agent table (migration 031)
- Replace DELETE /api/agents/{id} with POST /api/agents/{id}/archive
- Add POST /api/agents/{id}/restore endpoint
- ListAgents excludes archived by default (?include_archived=true to include)
- Skip archived agents in task triggers (on_assign, on_comment, on_mention)
- Block assignment to archived agents
- Cancel pending tasks on archive
- New events: agent:archived, agent:restored (replacing agent:deleted)

Frontend:
- Agent type includes archived_at/archived_by fields
- Mention autocomplete and assignee picker filter out archived agents
- Agent list shows archived agents with muted styling
- Agent detail shows archive banner with restore button
- Delete button replaced with Archive button and updated confirmation dialog
- API client: archiveAgent/restoreAgent replace deleteAgent

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

* fix(agent): self-review fixes for archive feature

- Fix: workspace store now fetches agents with include_archived=true
  so archived agents are actually visible in the frontend (the archived
  UI was dead code before — ListAgents excludes archived by default)
- Fix: add error logging for CancelAgentTasksByAgent in ArchiveAgent
- Fix: add idempotency guards — return 409 Conflict when archiving
  an already-archived agent or restoring a non-archived agent
- Fix: revert unnecessary extra GetAgent query in ReconcileAgentStatus
  (archived agents won't have running tasks after CancelAgentTasksByAgent)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:33:52 +08:00
Naiyuan Qing
565afed447 Merge pull request #345 from multica-ai/fix/link-sticky-cursor
fix(editor): prevent link mark from sticking to cursor
2026-04-02 17:01:35 +08:00
Naiyuan Qing
222f60d2dd fix(editor): prevent link mark from sticking to cursor
Override Link extension `inclusive: false` via `.extend()` to decouple
it from `autolink: true`. Tiptap's source ties `inclusive` to `autolink`,
causing typed text after a link to inherit the link mark.

Also set `linkOnPaste: false` — autolink's PasteRule still auto-detects
pasted URLs without the sticky cursor issue.

Refs: ueberdosis/tiptap#2571, ueberdosis/tiptap#4249

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:59:20 +08:00
Naiyuan Qing
e8c2a8eff9 Merge pull request #342 from multica-ai/NevilleQingNY/fetch-latest
feat(web): skeleton loading, error toasts, confirmation dialogs
2026-04-02 16:49:36 +08:00
Naiyuan Qing
7f0cb106bd feat(web): add skeleton loading, error toasts, and confirmation dialogs
- Replace all "Loading..." text with structured skeleton screens
  (Issue Detail, Agents, Skills, Runtimes, Tokens, Usage)
- Add toast.error for all API failures that were previously silent
  (Agents CRUD, Skills CRUD, workspace store, issue/inbox stores,
   timeline/reactions/subscribers hooks, agent-live-card)
- Add toast.success for mutations (agent update/delete, skill CRUD)
- Add confirmation dialogs for destructive actions
  (comment delete, token revoke)
- Add empty states for Issues and My Issues pages
- Fix hydrateWorkspace resilience: each request catches independently
  so partial failures don't block workspace entry
- Fix React key warning in issue-detail timeline rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:46:56 +08:00
Naiyuan Qing
c7fda85a3e Merge pull request #340 from multica-ai/feat/unified-tiptap-editor
feat(editor): unify Tiptap editor for editing and readonly display
2026-04-02 16:40:02 +08:00
Naiyuan Qing
b8b4731602 fix(editor): use ProseMirror schema for image upload state
Address code review feedback:
- Replace rAF + DOM query with Image extension `uploading` attribute
  managed by ProseMirror schema (no race conditions)
- Remove redundant removeAttribute call (setNodeMarkup rebuilds DOM)
- Restore pulse animation on img[data-uploading] for upload feedback
- Remove dev mock from use-file-upload.ts (was blocking real uploads)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:36:59 +08:00
Naiyuan Qing
fe975fb2bb feat(editor): unify Tiptap editor for editing and readonly display
Replace react-markdown in comment/reply display with Tiptap readonly mode,
ensuring visual consistency between editing and viewing. Extract shared
BaseMentionExtension with MentionView NodeView used in both modes — issue
mentions render as inline cards with StatusIcon, clickable to open in new tab.
Redesign mention suggestion popup with grouped sections (Users/Issues),
agent badges, and StatusIcon for issues.

- New: mention-extension.ts (shared mention core)
- New: mention-view.tsx (shared NodeView for both modes)
- New: readonly-editor.tsx (lightweight Tiptap readonly wrapper)
- Modified: rich-text-editor.tsx (import from shared mention-extension)
- Modified: rich-text-editor.css (readonly + issue-mention overrides)
- Modified: comment-card.tsx (Markdown → ReadonlyEditor)
- Modified: mention-suggestion.tsx (grouped UI, StatusIcon, agent badge)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:35:52 +08:00
LinYushen
fc0ef0fcd8 fix(ci): update backend Go version from 1.24 to 1.26.1 (#337)
Align the CI backend job with the Go version declared in server/go.mod
and used in the Dockerfile (golang:1.26-alpine).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:11:05 +08:00
Bohan Jiang
62b7c0cfa2 docs: add Quickstart section to README (#335)
Add an end-to-end onboarding guide covering login, daemon setup,
runtime verification, agent creation, and first task assignment.
Updated both English and Chinese READMEs.
2026-04-02 15:39:45 +08:00
LinYushen
606930725a feat(daemon): support direct download update for non-Homebrew installs (#334)
* feat(daemon): support direct download update for non-Homebrew installs

Previously, CLI auto-update only worked for Homebrew installations. Non-brew
binaries would fail with "not installed via Homebrew". Now the daemon and
`multica update` fall back to downloading the release binary directly from
GitHub Releases when Homebrew is not detected.

Also fixes:
- Daemon restart now uses the current executable's absolute path instead of
  searching PATH, ensuring the updated binary is used
- Brew installs preserve the symlink path so the new Cellar version is picked up
- Daemon startup logs now include the CLI version
- Update UI auto-clears "restarting" status after 5s to show the new version

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

* fix(cli): remove dead DetectNewBinaryPath and guard against nil latest version

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:38:06 +08:00
Naiyuan Qing
ed9aef8f39 Merge pull request #333 from multica-ai/agent/naiyuan-agent/074f02da
feat(web): add canonical URL, robots directive, and login tagline update
2026-04-02 14:50:51 +08:00
Naiyuan Qing
856a254252 fix(web): update login page test to match new tagline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:41:40 +08:00
LinYushen
682dc20ba9 fix(runtime): display multica CLI version instead of agent CLI version (#332)
The runtime detail page was showing the agent CLI version (claude/codex)
as "CLI Version" because metadata.version stored the agent version from
agent.DetectVersion(). The multica CLI version was never sent.

Fix: daemon now sends cli_version in the registration request, server
stores it as metadata.cli_version alongside the existing agent version,
and frontend reads metadata.cli_version.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:40:35 +08:00
Naiyuan Qing
17418f37b2 feat(web): update SEO metadata based on landing page
Align website title, description, and meta tags with the landing page
messaging. Add Open Graph, Twitter Card tags, sitemap.ts, and robots.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:28:28 +08:00
LinYushen
eba2e7eacf Merge pull request #327 from multica-ai/agent/j/53c0348f
feat(inbox): auto-scroll to comment and jump-to-bottom button
2026-04-02 14:28:26 +08:00
LinYushen
fdba410f11 feat(runtime): support CLI update from web runtime page (#331)
* feat(runtime): support CLI update from web runtime page

Add the ability to update the CLI daemon from the web Runtime detail page.
When a newer version is available on GitHub Releases, an update button
appears. Clicking it sends an update command through the server to the
daemon via the heartbeat mechanism (same pattern as ping). The daemon
executes `brew upgrade`, reports the result, and restarts itself with the
new binary.

Changes across all three layers:
- Frontend: version display, GitHub latest check, UpdateSection component
- Server: UpdateStore (in-memory), heartbeat extension, 3 new endpoints
- CLI: shared update logic, daemon handleUpdate + graceful restart

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

* fix(runtime): handle 'running' status in ReportUpdateResult

The daemon sends {"status":"running"} when it starts executing the
update, but ReportUpdateResult treated any non-"completed" status as
failure — immediately marking the update as failed before brew upgrade
even ran.

Fix: use a switch statement to handle "running" as a no-op (status is
already "running" from PopPending), and also timeout running updates
after 120 seconds in case brew upgrade hangs.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:12:49 +08:00
LinYushen
a80d61f8e1 fix(task): enforce per-issue serial execution in task claiming (#330)
Add NOT EXISTS check to ClaimAgentTask SQL to prevent claiming a queued
task when the same issue already has a dispatched/running task. This
ensures serial execution within an issue while preserving parallel
execution across different issues (concurrency group pattern).

Also add defensive guard in the frontend task:dispatch handler to avoid
replacing an active task's LiveLog timeline mid-execution.

Closes MUL-183

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:12:00 +08:00
LinYushen
ff616de82b fix(upload): remove file type allowlist to support all file types (#329)
* fix(upload): remove file type allowlist to support all file types

Removes the hardcoded MIME type allowlist from both frontend and backend
that was blocking uploads of file types like Word documents (.docx).
File size limit (10 MB) is still enforced. Content type detection is
preserved for metadata storage.

Closes MUL-123

* feat(upload): increase file size limit from 10 MB to 100 MB

Updates both frontend and backend to allow uploads up to 100 MB.
2026-04-02 13:55:50 +08:00
Bohan Jiang
f353e8db59 feat(mentions): support @mentioning issues + server-side auto-expansion (#242)
* feat(mentions): support @mentioning issues in comments

- Extend MentionItem type to include "issue" alongside "member"/"agent"
- Add issue search (by identifier and title) to mention suggestion dropdown
- Render issue mentions with CircleDot icon in autocomplete popup
- Issue mentions serialize as [MUL-117 Title](mention://issue/id) (no @ prefix)
- Markdown renderer shows issue mentions as clickable links to /issues/:id
- Backend mentionRe regex updated to match issue mention type

* feat(mentions): auto-expand issue identifiers and add mention format to agent instructions

1. Path A — CLAUDE.md template (runtime_config.go):
   Add a "## Mentions" section teaching agents the mention serialization
   format for issues, members, and agents. All agents automatically
   receive this via the auto-generated CLAUDE.md.

2. Approach 2 — Server-side auto-conversion (internal/mention/):
   New ExpandIssueIdentifiers() utility that scans comment content for
   bare issue identifiers (e.g. MUL-117) and replaces them with
   [MUL-117](mention://issue/<uuid>) mention links. Skips code blocks,
   inline code, and existing markdown links. Integrated into both:
   - handler.CreateComment (HTTP API path)
   - service.createAgentComment (agent task output path)
2026-04-02 13:48:53 +08:00
Jiang Bohan
575bbd7f60 feat(inbox): auto-scroll to comment from notification and add jump-to-bottom button
When clicking an inbox notification, the issue detail now scrolls to and
briefly highlights the relevant comment. Also adds a floating "Jump to
bottom" button on issue pages with long timelines.

Backend: store comment_id in inbox notification details for new_comment
and reaction_added events. Frontend: pass highlightCommentId through to
IssueDetail, add id attributes to comment elements, and track scroll
position for the jump-to-bottom button.
2026-04-02 13:43:05 +08:00
LinYushen
cd1b1155c1 Merge pull request #324 from multica-ai/fix/agent-permission-model
fix(agent): revise agent permission model for visibility and mentions
2026-04-02 12:51:09 +08:00
yushen
68da1efd74 fix(agent): revise agent permission model for visibility and mentions
- ListAgents: private agents are now visible to all workspace members
  (previously hidden from non-owner members)
- Mentions: private agents can only be @mentioned by the agent owner or
  workspace admin/owner; regular members' mentions of private agents are
  silently ignored
- Settings (update/delete/skills) and assign were already correctly
  restricted in previous PRs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:45:31 +08:00
LinYushen
bea739cba5 Merge pull request #322 from multica-ai/fix/agent-skill-permission
fix(agent): allow members to manage skills on their own agents
2026-04-02 12:36:50 +08:00
LinYushen
b88c7f1afa Merge pull request #321 from multica-ai/agent/yushen-claude/1fe82e4b
feat(web): extract Repositories into standalone settings tab
2026-04-02 12:26:14 +08:00
yushen
f05f3face3 fix(agent): allow members to manage skills on their own agents
SetAgentSkills previously only allowed workspace owner/admin roles,
blocking members from adding skills to their own agents. Now uses
canManageAgent which allows agent owners too.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:25:19 +08:00
yushen
bf713d3ad5 feat(web): extract Repositories into standalone settings tab
Move the Repositories section from the General workspace settings page
into its own dedicated tab in the Settings sidebar, making it a
first-class entry alongside General and Members. This reduces the
navigation depth from 3 clicks + scroll to a single click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:14:59 +08:00
LinYushen
ac06e7f4a3 fix(agent): restrict agent management to owner and workspace admins (#320)
Members could previously modify any workspace-visible agent. Now only
the agent owner or workspace owner/admin can update or delete an agent,
regardless of visibility.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:13:45 +08:00
Jiayuan Zhang
0659865645 Merge pull request #314 from multica-ai/agent/lambda/0fa89561
feat(cli): add issue runs and run-messages commands
2026-04-02 03:37:16 +08:00
Jiayuan
ab505fd39c docs: add issue runs and run-messages to CLI documentation
Update the dynamic agent instructions (runtime_config.go) and the
CLI reference (CLI_AND_DAEMON.md) to document the new execution
history commands.
2026-04-02 03:34:25 +08:00
Jiayuan Zhang
7b19ad7ccc Merge pull request #313 from multica-ai/agent/lambda/9c6590aa
fix(server): broadcast sweeper task:failed events to correct workspace
2026-04-02 03:33:37 +08:00
Jiayuan
6b0c9bba9e feat(cli): add issue runs and run-messages commands
Add two new CLI commands so agents can access execution history:
- `multica issue runs <issue-id>` lists all task executions for an issue
- `multica issue run-messages <task-id>` lists messages for an execution

Also adds --since query param support to the ListTaskMessages backend
handler for incremental message fetching.
2026-04-02 03:30:33 +08:00
Jiayuan
60e66f32b3 test(server): add integration tests for sweeper event broadcasting fix
Tests verify:
- Stale running tasks: task:failed event has WorkspaceID set
- Agent status reconciliation: agent returns to "idle" after sweep
- Stale dispatched tasks: same correctness for dispatch timeout path
2026-04-02 03:30:32 +08:00
Jiayuan
eb35bc5dc9 fix(server): broadcast sweeper task:failed events to correct workspace
The runtime sweeper was publishing task:failed events without a
WorkspaceID, causing them to be silently dropped by the WS listener.
This meant frontends never received notification when stale/orphaned
tasks were failed by the sweeper — the live log card kept showing
"Agent is working" and the agent status remained "working" indefinitely.

- Look up workspace_id from issue table for each swept task
- Set WorkspaceID on published events so they reach the correct WS room
- Reconcile agent status after sweeping so agents return to "idle"
2026-04-02 03:20:59 +08:00
Jiayuan Zhang
99fdf39c50 Merge pull request #265 from multica-ai/agent/lambda/f37e2ab8
docs: redesign README for public repo
2026-04-02 01:59:21 +08:00
Jiayuan
e4b2053d90 docs: align README messaging with landing page copy
- Replace "project management platform" framing with landing page
  positioning: "turns coding agents into real teammates"
- Use landing page hero subheading for tagline
- Rewrite "What is Multica?" to emphasize agent autonomy, not PM features
- Update features to match landing page sections (autonomous execution,
  reusable skills, unified runtimes)
- Apply same changes to Chinese README using landing page zh translations
2026-04-02 01:56:29 +08:00
Jiayuan
1264a1fa67 docs: fix Cloud link and add Chinese README
- Change Cloud link from app.multica.ai to multica.ai/app
- Add README.zh-CN.md with full Chinese translation
- Add language switcher (English | 简体中文) to both READMEs
2026-04-02 01:46:16 +08:00
Jiayuan
c4a24c1a86 docs: refine README layout per feedback
- Move banner image to very top, remove feature background images
- Move product screenshot into "What is Multica?" section
- Fix logo SVG: use icon-only (no text) for reliable GitHub rendering
- Use markdown heading for "Multica" text instead of SVG text element
- Clean up features into a single bullet list
2026-04-02 01:09:38 +08:00
Jiayuan
479d69305d docs: redesign README with landing page content and images
- Add centered header with logo (dark/light mode), badges, and nav links
- Use landing page headline "Your next 10 hires won't be human."
- Add hero banner illustration and product screenshot from landing page
- Feature sections with inline images (teammates, runtimes)
- Rewrite feature descriptions to match landing page messaging
- Add architecture table alongside diagram
- Create logo SVGs in docs/assets/
- All links verified against main branch (CLI_AND_DAEMON.md, CONTRIBUTING.md, etc.)
2026-04-02 00:46:02 +08:00
Jiayuan Zhang
85d00fde57 Merge pull request #312 from multica-ai/fix/at-all-suppress-trigger
fix(server): @all mentions should not trigger agent execution
2026-04-02 00:37:26 +08:00
Jiayuan
02c9d9f0b0 fix(server): @all mentions should not trigger agent execution
@all is a broadcast to all workspace members — it should not trigger
the assignee agent's on_comment. Previously @all was treated as
"includes everyone" and allowed the trigger.

Changes:
- commentMentionsOthersButNotAssignee now checks HasMentionAll() early
  and returns true (suppress) when @all is present
- Fix authRequestWithAgent test helper that was making a duplicate HTTP
  request (one as member, one as agent)

Tests: 5 new @all unit test cases, 2 new @all integration test cases.
2026-04-02 00:33:21 +08:00
Jiayuan Zhang
05fcf35ab9 Merge pull request #311 from multica-ai/agent/lambda/18dbd9cb
docs(web): add v0.1.4 changelog for 2026-04-01
2026-04-02 00:27:35 +08:00
Jiayuan
f315e55cd6 docs(web): add v0.1.4 changelog entry for 2026-04-01
Add changelog for April 1st releases covering My Issues page,
i18n support, about/changelog pages, agent avatars, attachment
improvements, unified avatar rendering, and Apache 2.0 license.
2026-04-02 00:24:04 +08:00
Jiayuan
b1f7364097 fix(web): navigate to /issues when switching workspaces
When switching workspaces while on a detail page (e.g. /issues/[id]),
the store clears old data and the page tries to fetch the old resource
with the new workspace context, causing a 404 error. Navigate to the
issues list before switching to avoid referencing stale resources.
2026-04-02 00:15:47 +08:00
Jiayuan Zhang
8f1526d2bb Merge pull request #306 from multica-ai/agent/lambda/832eb090
fix(server): improve comment trigger logic for agent execution
2026-04-01 22:28:52 +08:00
Naiyuan Qing
9add49b832 Merge pull request #308 from multica-ai/revert-307-feat/global-search
Revert "feat: add global issue search"
2026-04-01 22:24:58 +08:00
Naiyuan Qing
b28bac6bb7 Revert "feat: add global issue search" 2026-04-01 22:24:35 +08:00
Naiyuan Qing
59ebf30cf0 Merge pull request #307 from multica-ai/feat/global-search
feat: add global issue search
2026-04-01 22:13:17 +08:00
Jiayuan
b41536467d test(server): add integration tests for comment trigger logic
End-to-end tests through the full HTTP router + real database:

TestCommentTriggerOnComment (6 subtests):
- Top-level comment without mentions → triggers agent
- Top-level comment mentioning only others → suppresses trigger
- Top-level comment mentioning assignee → triggers agent
- Reply to agent thread without mentions → triggers agent
- Reply to member thread without mentions → suppresses trigger (Bohan's bug)
- Reply to member thread mentioning assignee → triggers agent

TestCommentTriggerOnAssignNoStatusGate:
- Assigning agent to in_progress issue → triggers (no todo restriction)

TestCommentTriggerOnMentionNoStatusGate:
- @mentioning agent on done issue → triggers (no status gate)

TestCommentTriggerCoalescing:
- Rapid-fire comments → only 1 task created (dedup)
2026-04-01 22:11:46 +08:00
Naiyuan Qing
40d29bea50 feat: add global issue search with sidebar button and modal
Add search functionality to quickly find issues by title:
- Backend: add search param (ILIKE) to ListIssues query
- Frontend: search modal using CommandDialog with skeleton loading
- Sidebar: ghost-style search button next to create issue button
- Handle CJK input method composition to avoid premature searches
- Responsive max-height for small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:11:28 +08:00
Jiayuan
4f0c2afcb7 test(server): add unit tests for comment trigger logic
Tests cover all on_comment trigger decision scenarios:
- commentMentionsOthersButNotAssignee: 6 cases
- isReplyToMemberThread: 7 cases
- Combined trigger decision: 9 cases matching the full decision table
- agentHasTriggerEnabled: 7 cases (nil, empty, explicit, backwards compat)
- defaultAgentTriggers: validates all 3 triggers present and enabled
2026-04-01 22:01:33 +08:00
Jiayuan
eb5aaf003c fix(server): remove status gates from on_assign and on_mention triggers
on_assign: Remove the todo-only restriction. Assignment is an explicit
human action — if someone assigns an agent to a done/in_progress issue,
they want the agent triggered (e.g. to fix a problem found after close).

on_mention: Remove the done/cancelled check. @mention is an explicit
action and should work on any issue status. The agent can reopen the
issue if needed.
2026-04-01 21:46:20 +08:00
Jiayuan
04da4bc3b7 fix(server): improve comment trigger logic for agent execution
- Add thread-aware on_comment suppression: when a member replies in a
  thread started by another member without @mentioning the assignee
  agent, the on_comment trigger is now suppressed. This fixes the bug
  where member-to-member conversations incorrectly triggered the
  assigned agent.

- Add terminal status check to on_mention: enqueueMentionedAgentTasks
  now skips done/cancelled issues, consistent with on_comment behavior.

- Write explicit default triggers on agent creation: new agents get
  [on_assign, on_comment, on_mention] all enabled, instead of relying
  on null/empty = all enabled. Existing agents with empty triggers
  still work via backwards-compat fallback in agentHasTriggerEnabled.

- Consolidate trigger check logic into shared agentHasTriggerEnabled
  helper, fixing inconsistency where empty [] was handled differently
  by isAgentTriggerEnabled (returned false) vs isAgentMentionTriggerEnabled
  (returned true).

- Add documentation comments explaining the intentional status gate
  difference: on_assign fires only for todo (start new work), while
  on_comment fires for any non-terminal status (conversational).
2026-04-01 21:37:33 +08:00
Jiayuan Zhang
6b9341f7ad Merge pull request #304 from multica-ai/agent/lambda/95b033ec
feat(mentions): support @all to mention all workspace members
2026-04-01 21:03:28 +08:00
Jiayuan
095b7f8185 feat(mentions): support @all to mention all workspace members
Add @all mention type that notifies all workspace members (excluding
agents). Includes backend parsing, notification expansion to all members,
and frontend UI with autocomplete suggestion, rendering, and hover card.
2026-04-01 20:58:33 +08:00
Naiyuan Qing
e68091e4a8 Merge pull request #302 from multica-ai/fix/header-button-style-consistency
fix(web): mention picker avatar & keyboard scroll
2026-04-01 19:36:36 +08:00
Naiyuan Qing
8815d27e1d fix(web): use ActorAvatar in mention picker and fix keyboard scroll
- Replace inline initials/Bot icon with ActorAvatar component so
  mention suggestions show real profile pictures consistently
- Add scrollIntoView on keyboard navigation so the selected item
  stays visible when the list overflows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:35:23 +08:00
Naiyuan Qing
71dec10201 Merge pull request #300 from multica-ai/fix/header-button-style-consistency
fix(web): unify header button style consistency
2026-04-01 19:07:19 +08:00
Naiyuan Qing
d602873a5c fix(web): unify header button text color across Issues and My Issues
Right-side icon buttons (filter, display, view) were using default foreground
color while left-side scope buttons used text-muted-foreground, causing visual
inconsistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:05:33 +08:00
yushen
9bcc35bf61 docs: add CLI release instructions to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:54:18 +08:00
Naiyuan Qing
ae1c05af60 Merge pull request #293 from multica-ai/feature/issues-scope-tabs
feat(web): add scope tabs to Issues and My Issues pages
2026-04-01 18:49:07 +08:00
Naiyuan Qing
2b1b588187 Merge pull request #294 from multica-ai/NevilleQingNY/merge-dev-to-main
Merge dev into main
2026-04-01 18:43:27 +08:00
Naiyuan Qing
75b25539ab fix(web): remove duplicate ActorAvatar import from agents page
Auto-merge introduced a duplicate import line — removed the extra one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:32:29 +08:00
Naiyuan Qing
815f16ddd6 Merge remote-tracking branch 'origin/dev' into NevilleQingNY/merge-dev-to-main
# Conflicts:
#	apps/web/features/issues/components/issue-detail.tsx
#	apps/web/features/issues/components/issues-header.tsx
#	apps/web/features/issues/components/pickers/assignee-picker.tsx
2026-04-01 18:31:28 +08:00
Naiyuan Qing
8719e2cedd feat(web): add scope tabs to Issues and My Issues pages
Redesign both Issues and My Issues headers with Linear-style layout:
- Left: scope pill buttons (All/Members/Agents for Issues; Assigned/Created/My Agents for My Issues)
- Right: compact icon buttons for Filter, Display, and View toggle
- Selected scope has accent background, all buttons use consistent outline variant
- Filter active indicator uses brand-colored dot
- Tooltips on all buttons with 500ms global delay
- Remove New Issue button and issue count from headers
- Scope selection persisted to localStorage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:30:40 +08:00
Naiyuan Qing
808a0e9c38 Merge pull request #289 from multica-ai/NevilleQingNY/auth-root-redirect
feat(web): server-side redirect / → /issues for logged-in users
2026-04-01 18:25:49 +08:00
Naiyuan Qing
f1e5bc7925 feat(web): redirect logged-in users from / to /issues via server-side proxy
Use a lightweight cookie (multica_logged_in) + Next.js 16 proxy to
302-redirect authenticated users visiting / straight to /issues.
Unauthenticated visitors (and search engine crawlers) continue to see
the full landing page with zero flash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:20:57 +08:00
LinYushen
20a2332c94 Merge pull request #285 from multica-ai/feature/attachment-improvements
feat: improve attachment support across issue and comment APIs
2026-04-01 18:11:57 +08:00
yushen
9c249f0770 feat(server,cli): improve attachment support across issue and comment APIs
- Add --attachment flag to `multica issue create` CLI command
- Fix CreateComment response to include linked attachments instead of empty array
- Include attachments inline in GetIssue API response (matching Jira/ClickUp pattern)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:10:43 +08:00
Naiyuan Qing
8db970cd17 Merge pull request #270 from multica-ai/fix/avatar-consistency-clean
refactor(web): unify all avatar rendering with ActorAvatar
2026-04-01 17:23:19 +08:00
Naiyuan Qing
a18a24f904 Merge pull request #273 from multica-ai/NevilleQingNY/my-issues-kanban
feat(web): My Issues kanban board + list view + filtering
2026-04-01 17:17:26 +08:00
Naiyuan Qing
421e0a2429 Merge pull request #272 from multica-ai/feature/homepage-seo-optimization
feat(web): SEO optimization and auth flow improvements
2026-04-01 17:11:14 +08:00
Naiyuan Qing
09376dc879 Merge pull request #269 from multica-ai/NevilleQingNY/my-issues-kanban
feat(web): My Issues kanban board + list view + filtering
2026-04-01 17:02:33 +08:00
Naiyuan Qing
4d74091f8d refactor(web): unify all avatar rendering with ActorAvatar
Replace all inline avatar implementations (initials divs, Bot icons,
inline img tags) with the shared ActorAvatar component for consistency.

- Extend AssigneePicker with controlled open/onOpenChange, triggerRender,
  and align props to support batch toolbar and other contexts
- Replace BatchAssigneePicker (~130 lines) with shared AssigneePicker
- Replace issue-detail sidebar inline DropdownMenu with AssigneePicker
- Add canAssignAgent filtering to issue-detail more menu
- Replace inline avatars in: filter panel, members-tab, agents page,
  mention-hover-card, subscribers AvatarGroup
- Add data-slot="avatar" to ActorAvatar for AvatarGroup compatibility
- Add triggerRender prop to PropertyPicker for custom trigger elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:58:43 +08:00
Naiyuan Qing
fb8829fc12 Merge pull request #268 from multica-ai/feature/homepage-seo-optimization
feat(web): SEO optimization and auth flow improvements
2026-04-01 16:57:44 +08:00
Naiyuan Qing
3c5a3b5e6a feat(web): add kanban board + list view + filtering to My Issues page
Upgrade /my-issues from a simple accordion to a full-featured view
matching /issues — kanban board, list view, status/priority filtering,
sorting, and display settings, scoped to the user's own issues.

Key changes:
- Extract view store factory (createIssueViewStore) using zustand v5
  vanilla createStore + React Context for shared component reuse
- Create ViewStoreProvider + useViewStore/useViewStoreApi hooks
- Decouple BoardView, BoardColumn, BoardCard, ListView from global
  useIssueViewStore — they now read from context
- New independent persisted store for /my-issues (multica_my_issues_view)
- Simplified MyIssuesHeader (no assignee/creator filters)
- Pre-filter logic: assigned to me ∪ my agents ∪ created by me
- Generalize workspace sync to clear filters on all registered stores
- Fix existing debt: text-[10px] → text-xs, w-44 → w-auto, reduce
  unnecessary selector subscriptions in both headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:56:22 +08:00
Naiyuan Qing
3bd504fe5f feat(web): SEO optimization and auth flow improvements for landing pages
- Split landing pages into Server/Client Components to enable Next.js metadata exports
- Add robots.ts, sitemap.ts, JSON-LD structured data, OpenGraph and viewport config
- Fix i18n hydration mismatch: detect locale server-side via cookie/Accept-Language header
- Replace localStorage with cookie for locale persistence (SSR-readable)
- Dynamic <html lang> based on locale cookie
- Optimize images with next/image (avif/webp formats, quality config)
- Add /homepage route as always-visible landing page (no auth redirect)
- Auth-aware CTA buttons: show "Dashboard"/"进入工作台" for logged-in users
- Login page redirects authenticated users to dashboard
- Unify logout/401 redirect to "/" instead of "/login"
- Fix title template to avoid double "Multica" suffix on homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:54:11 +08:00
LinYushen
39c5cf2cbe Merge pull request #261 from multica-ai/agent/j/7ba59661
feat(agents): add avatar upload to agent settings page
2026-04-01 16:18:00 +08:00
LinYushen
005025b05c fix(server): allow @agent mentions to trigger regardless of issue status (#267)
Remove terminal status (done/cancelled) checks that blocked @agent
mention triggers and task claiming. Agents should always be triggerable
via explicit @mentions, regardless of the issue's current status.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:15:59 +08:00
Jiayuan Zhang
7204626f1d Merge pull request #262 from yihong0618/hy/make_ascii_show_better
docs: make ascii Architecture show better
2026-04-01 16:11:19 +08:00
Naiyuan Qing
57cb32f09a Merge pull request #256 from multica-ai/fix/assignee-avatar-consistency
refactor(web): unify assignee dropdowns with ActorAvatar
2026-04-01 16:04:34 +08:00
Naiyuan Qing
1e4cd346ef fix(web): resolve agents/page.tsx merge conflict with dev (avatar upload feature)
Keep ActorAvatar for agent list/detail avatars over dev's inline
img+initials. Also unify the new avatar upload preview to use
ActorAvatar instead of getInitials helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:03:17 +08:00
Bohan Jiang
f933b5f3a7 Merge pull request #266 from multica-ai/agent/j/7ba59661
feat(agents): add avatar upload to agent settings page
2026-04-01 16:00:13 +08:00
LinYushen
3e96240cec docs: align AGENTS.md with CLAUDE.md content (#263)
AGENTS.md was a minimal 17-line summary while CLAUDE.md had comprehensive
project documentation. Updated AGENTS.md to include all sections from
CLAUDE.md (architecture, state management, backend structure, UI/UX rules,
worktree support, E2E patterns, verification loop) while preserving
AGENTS.md-unique details (naming conventions, test directory, PR guidelines).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:58:20 +08:00
Jiayuan Zhang
1bc491fc65 Merge pull request #253 from theowenyoung/patch-1
Update installation command for multica-cli
2026-04-01 15:57:30 +08:00
LinYushen
98e7d27acc feat(cli): add --attachment flag to issue comment add (#260)
Add file attachment support to `multica issue comment add`. The CLI
uploads files via multipart form to /api/upload-file, collects the
returned attachment IDs, and passes them when creating the comment.

Usage: multica issue comment add <issue-id> --content "..." --attachment file1.png --attachment file2.pdf

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:57:23 +08:00
Naiyuan Qing
cc922367be fix(web): resolve merge conflicts with dev and unify remaining inline avatars
Merge dev into fix/assignee-avatar-consistency, keeping our unified
AssigneePicker over dev's inline DropdownMenu additions.

Also replace remaining inline avatar implementations with ActorAvatar:
- members-tab.tsx: member list row initials → ActorAvatar
- agents/page.tsx: agent list item & detail header initials → ActorAvatar
- mention-hover-card.tsx: inline Bot icon → ActorAvatar
- issue-detail.tsx: subscribers AvatarGroup fallback → ActorAvatar
- actor-avatar.tsx: add data-slot="avatar" for AvatarGroup compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:52:48 +08:00
yihong0618
c36b6f3e07 docs: make ascii Architecture show better
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2026-04-01 15:52:15 +08:00
Jiang Bohan
ab48eafe61 feat(agents): add avatar upload to agent settings page
Add avatar upload UI to the agent SettingsTab, matching the existing
member avatar upload pattern. Also update the agent list item and
detail header to display the uploaded avatar image.
2026-04-01 15:49:01 +08:00
Bohan Jiang
5ec4bcd627 Merge pull request #241 from multica-ai/agent/j/7fc3e0e2
feat(agent): add task cancellation with stop button
2026-04-01 15:43:37 +08:00
Naiyuan Qing
f891a5bbd7 refactor(web): unify assignee dropdowns with ActorAvatar and shared AssigneePicker
- Replace inline initials/Bot divs with ActorAvatar across all assignee UIs
- Replace issue-detail sidebar DropdownMenu with shared AssigneePicker
- Delete BatchAssigneePicker (~130 lines), reuse AssigneePicker in controlled mode
- Add controlled mode (open/onOpenChange), align, and triggerRender props to AssigneePicker/PropertyPicker
- Add canAssignAgent visibility check to issue-detail more menu
- Clean up unused imports (Bot, useAuthStore, useWorkspaceStore, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:37:33 +08:00
Bohan Jiang
daaa4deaf7 Merge pull request #255 from multica-ai/agent/j/7fc3e0e2
Agent/j/7fc3e0e2
2026-04-01 14:58:54 +08:00
Naiyuan Qing
fe963d672d Merge pull request #254 from multica-ai/NevilleQingNY/sync-remote
fix(web): delay TitleEditor autofocus for Dialog animation
2026-04-01 14:35:00 +08:00
Owen
0d214dc321 Update installation command for multica-cli 2026-04-01 14:28:18 +08:00
Naiyuan Qing
946b4a277e fix(web): delay TitleEditor autofocus to avoid Dialog animation conflict
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:15:31 +08:00
Jiayuan Zhang
a2eb16995e Merge pull request #251 from multica-ai/forrestchang-patch-1
fix(docs): Update URL for getting started with Multica
2026-04-01 14:11:41 +08:00
Jiayuan Zhang
094962d9aa fix(docs): Update URL for getting started with Multica 2026-04-01 14:11:24 +08:00
Jiayuan Zhang
9a00fd5146 Merge pull request #250 from multica-ai/agent/lambda/4c494a4f
docs: add CLI/Daemon guide and rename LOCAL_DEVELOPMENT → CONTRIBUTING
2026-04-01 14:07:07 +08:00
Jiayuan
0f2dfdbca8 docs: add CLI/Daemon guide and rename LOCAL_DEVELOPMENT.md to CONTRIBUTING.md
- Add CLI_AND_DAEMON.md with full command reference, daemon configuration,
  profiles, and self-hosted setup instructions
- Rename LOCAL_DEVELOPMENT.md → CONTRIBUTING.md for conventional naming
- Update README.md references and link to the new CLI guide
2026-04-01 14:05:19 +08:00
Jiayuan Zhang
c6e67bd41e Merge pull request #249 from multica-ai/agent/lambda/32ad91e3
chore: change license to Apache 2.0
2026-04-01 13:46:24 +08:00
yushen
1a0d8c282a feat(cli): default to production URLs (api.multica.ai, multica.ai)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:27:58 +08:00
Jiayuan
c52c6e69c3 chore: change license to Apache 2.0
Add Apache License 2.0 LICENSE file and update .goreleaser.yml
reference from MIT to Apache-2.0.
2026-04-01 13:18:57 +08:00
Jiang Bohan
4780540bd2 merge: resolve conflicts with main 2026-04-01 13:12:23 +08:00
Bohan Jiang
8218d7e87d Merge pull request #247 from multica-ai/fix/issue-mention-rendering
fix(issues): use Markdown component for read-only comment display
2026-04-01 13:06:27 +08:00
Naiyuan Qing
57e48c1d6b Merge pull request #246 from multica-ai/forrestchang/landing-page
feat(web): add landing page with i18n (EN/中文)
2026-04-01 08:20:53 +08:00
Naiyuan Qing
afdefa9b65 Merge branch 'dev' of https://github.com/multica-ai/multica into dev 2026-04-01 08:19:40 +08:00
Naiyuan Qing
a4d222da46 Merge branch 'forrestchang/landing-page' into dev 2026-04-01 08:19:11 +08:00
Jiayuan
bf0765de26 chore(web): update next-env.d.ts routes path 2026-04-01 05:51:28 +08:00
Jiayuan
7e2b328e3c feat(web): add i18n support to landing page with Simplified Chinese
- Add dictionary-based i18n system (en/zh) with React Context provider
- Extract all hardcoded text from landing components into translation dictionaries
- Auto-detect browser language, persist preference in localStorage
- Add language switcher (EN/中文) to footer
- Add Noto Serif SC font for Chinese serif headings
- Rewrite about page Chinese copy with user-provided translation
2026-04-01 05:47:40 +08:00
Jiayuan
0b4c6b3910 feat(web): add about, changelog pages and fix landing header for light backgrounds
- Rewrite about page with Multica name origin (Multics → multiplexed
  agents) and project philosophy
- Replace placeholder changelog with real entries from git history
  (v0.1.0–v0.1.3)
- Add variant prop to LandingHeader (dark/light) so it renders
  correctly on white-background subpages
- Extract landing page into separate component files
2026-04-01 05:16:24 +08:00
Jiayuan Zhang
82e45fb0a2 Merge pull request #245 from multica-ai/forrestchang/fix-dropdown-avatars
fix(ui): show avatar images in assignee dropdowns
2026-04-01 04:29:27 +08:00
Jiayuan
c0166e5264 fix(ui): show avatar images in assignee dropdowns
Replace inline div placeholders (initials/Bot icon) with ActorAvatar
component so actual user and agent avatar images render in all assignee
picker dropdowns.
2026-04-01 03:50:47 +08:00
Jiayuan
e9ce376b96 feat(web): add how-it-works, open-source, FAQ, and footer sections to landing page
Add four new sections: How it Works (4-step onboarding guide), Open Source
(self-host / no lock-in highlights), FAQ (centered accordion), and Footer
(link columns + giant logo with Game of Life animation).
2026-04-01 00:18:25 +08:00
Jiayuan
11041052d0 feat(web): add features section and product hero image to landing page
Add scroll-driven features section with 4 tabs (Teammates, Autonomous,
Skills, Runtimes) using IntersectionObserver for auto-switching on scroll.
Replace hero placeholder with actual product screenshot. Remove background
gradient overlay.
2026-03-31 23:02:23 +08:00
Jiayuan
99ac6a6736 feat(web): add multica landing page 2026-03-31 22:18:19 +08:00
Bohan Jiang
ed3bcaea6b Merge pull request #244 from multica-ai/fix/issue-mention-rendering
fix(issues): use Markdown component for read-only comment display
2026-03-31 19:29:12 +08:00
Jiang Bohan
449011e60b fix(issues): use Markdown component for read-only comment display
The RichTextEditor in read-only mode doesn't support IssueMentionCard
rendering for issue mentions with status, title, and navigation.
Switch to the Markdown component which already handles mention://issue/
links with the IssueMentionCard component.
2026-03-31 19:25:31 +08:00
Naiyuan Qing
16efcaca43 Merge pull request #243 from multica-ai/fix/inbox-header-scroll
fix(inbox): pin header and only scroll content in list panel
2026-03-31 19:17:57 +08:00
Naiyuan Qing
e2453cc040 fix(inbox): pin header and only scroll content in inbox list panel
The inbox left panel had the header inside the overflow-y-auto container,
causing it to scroll away with the list items. Changed to flex-col layout
with shrink-0 header and flex-1 overflow-y-auto content area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:16:23 +08:00
Naiyuan Qing
b589d733ca fix(inbox): pin header and only scroll content in inbox list panel
The inbox left panel had the header inside the overflow-y-auto container,
causing it to scroll away with the list items. Changed to flex-col layout
with shrink-0 header and flex-1 overflow-y-auto content area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:13:47 +08:00
Bohan Jiang
62eafe5f99 Merge pull request #237 from multica-ai/agent/j/f5e0099c
fix(server): subscribe @mentioned users in issue descriptions
2026-03-31 19:00:07 +08:00
Jiayuan Zhang
7188fd8f2a Merge pull request #236 from multica-ai/agent/lambda/b34769c1
fix(issues): show execution logs for mention-triggered agent tasks
2026-03-31 18:52:19 +08:00
Jiayuan Zhang
46ea12e4cf Merge pull request #238 from multica-ai/agent/lambda/6279e21b
fix(inbox): remove hardcoded 50-item limit from inbox list query
2026-03-31 18:44:51 +08:00
Jiang Bohan
365b5b5f0e feat(agent): add task cancellation with stop button
Users can now interrupt running agents via a "Stop" button on the live
card. The daemon polls task status every 5 seconds and kills the agent
process when cancellation is detected.

Changes:
- New CancelAgentTask SQL query and CancelTask service method
- POST /api/issues/{id}/tasks/{taskId}/cancel endpoint
- Daemon polls GetTaskStatus during execution, cancels context on match
- Frontend: Stop button on AgentLiveCard, task:cancelled WS event
2026-03-31 18:43:37 +08:00
Naiyuan Qing
4c3d9ed1a1 Merge pull request #239 from multica-ai/feature/editor-ux-improvements
feat(ui): editor UX improvements — lowlight, upload, emoji, comment editing
2026-03-31 18:39:36 +08:00
Naiyuan Qing
9a37af4ca1 feat(ui): editor UX improvements — lowlight, upload, emoji, comment editing
- New QuickEmojiPicker: shared SmilePlus + 8 quick emojis + full picker
- New FileUploadButton: reusable Paperclip upload trigger
- New CodeBlockView: React NodeView with language label + copy button
- CodeBlockLowlight: syntax highlighting in editor (replaces plain codeBlock)
- ReactionBar: brand-tinted pill styles, hideAddButton prop
- Comment header: emoji picker + three-dot menu in top-right
- Comment edit: inline editing with brand border, blur-to-save, Escape-to-cancel
- RichTextEditor: add onBlur prop, markdown paste extension
- Create issue: upload button in footer
- Issue detail: upload button next to reaction bar
- Comment/reply: use FileUploadButton, loading spinners, no optimistic updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:37:53 +08:00
Jiayuan
38cad92e7e fix(inbox): remove hardcoded 50-item limit from inbox list query
The ListInbox endpoint defaulted to LIMIT 50 while the frontend fetched
all items without pagination, causing items beyond the first 50 to be
silently dropped.
2026-03-31 18:36:41 +08:00
Jiang Bohan
c8ab2e8642 fix(server): subscribe @mentioned users in issue descriptions
When a user @mentions someone in an issue description (create or update),
the mentioned users were notified via inbox but not added as subscribers.
This adds subscriber registration for mentioned users in both the
issue:created and issue:updated event listeners, mirroring the existing
notification logic.
2026-03-31 18:36:15 +08:00
Jiayuan
68ca281761 test(issues): add missing api mocks for agent task endpoints
Add getActiveTaskForIssue, listTasksByIssue, and listTaskMessages mocks
to the issue detail page test. These are now called unconditionally since
the assigneeType guard was removed.
2026-03-31 18:33:23 +08:00
Jiayuan
0431ee2ee0 fix(issues): show execution logs for mention-triggered agent tasks
AgentLiveCard and TaskRunHistory were gated on assigneeType === "agent",
so mention-triggered tasks on non-agent-assigned issues never showed
their execution logs. Remove that guard so any issue with agent tasks
displays live output and execution history.

Also populate IssueID in ListTaskMessages response so the live card's
WS event filtering works correctly on catch-up after reconnect.
2026-03-31 18:27:37 +08:00
Jiayuan Zhang
3f612c37f2 Merge pull request #230 from multica-ai/agent/lambda/bba7b0bd
fix(web): show executing agent name in task log title
2026-03-31 17:56:54 +08:00
Jiayuan Zhang
00af622d0f Merge pull request #235 from multica-ai/agent/lambda/d99edc96
docs: rewrite README and add self-hosting guide
2026-03-31 17:56:10 +08:00
Jiayuan
400739f3c9 docs: rewrite README for users and add self-hosting guide
Rewrite README.md from a developer-focused quick start to a user-facing
project overview covering features, cloud vs self-host, CLI usage, and
architecture. Add SELF_HOSTING.md with complete deployment instructions
including configuration reference, database setup, reverse proxy examples
(Caddy/nginx), agent daemon setup, and upgrade procedures.
2026-03-31 17:51:05 +08:00
Naiyuan Qing
03d2a5546c Merge pull request #232 from multica-ai/fix/inbox-issue-detail-key
fix(inbox): reset IssueDetail state on issue switch
2026-03-31 17:17:41 +08:00
Naiyuan Qing
9a6925bc8b fix(agents): add key to AgentDetail to reset state on agent switch
Same issue as inbox: without a key, React reuses the AgentDetail
instance when switching agents. This causes activeTab and SettingsTab
form values (name, description, visibility, maxTasks) to persist
from the previously selected agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:07:23 +08:00
Bohan Jiang
d0ad32a5b9 Merge pull request #233 from multica-ai/agent/j/298fd11b
feat(web): redesign issue creation toast to match Linear
2026-03-31 17:04:48 +08:00
Jiayuan
f2289bd733 fix(test): make createComment assertion resilient to trailing undefined args
On CI (Node 22 / ubuntu), mockCreateComment receives extra undefined
args. Check only the first two positional args instead of exact match.
2026-03-31 17:04:29 +08:00
Jiang Bohan
68217d2967 feat(web): redesign issue creation toast to match Linear's style
Replace the small one-line sonner toast with a richer card showing:
- Green checkmark with "Issue created" title
- Status icon + issue identifier + title on second line
- Clickable "View issue" link to navigate to the new issue
2026-03-31 17:02:23 +08:00
Jiayuan
259f2823bc fix(web): show executing agent name in task log, not assigned agent
When a task is executed by a mentioned agent (not the assigned one),
the live card now resolves the agent name from activeTask.agent_id
instead of using the assignee-based agentName prop.
2026-03-31 16:58:50 +08:00
Jiayuan
c902460eae fix(server): suppress assignee on_comment when mentions target others
When a comment @mentions anyone but not the assignee agent, the
assignee's on_comment trigger is now suppressed. This prevents the
assignee agent from being re-triggered when users share results with
colleagues or ask other agents for help.

The rule: @mention is an intent signal — if you're talking to someone
else, the assignee agent should not respond.
2026-03-31 16:58:50 +08:00
Jiayuan
3646ec5a53 feat(server): trigger agents via @mention in comments
When a user @mentions an agent in any issue's comment, the system now
enqueues a task for that agent. The agent reads the issue context and
replies to the triggering comment thread.

Changes:
- Add shared util.ParseMentions for mention parsing (used by both
  comment handler and notification listeners)
- Add EnqueueTaskForMention to TaskService for explicit agent targeting
- Add on_mention trigger type support in agent trigger config
- Add HasPendingTaskForIssueAndAgent SQL query for per-agent dedup
- Add enqueueMentionedAgentTasks in CreateComment handler

Safety: prevents self-trigger (agent mentioning itself), dedup with
assignee on_comment trigger, terminal issue status check, and per-agent
pending task dedup.
2026-03-31 16:58:50 +08:00
Naiyuan Qing
e2c466ffa1 fix(inbox): add key to IssueDetail to reset state on issue switch
Without a key, React reuses the IssueDetail component instance when
switching between inbox items. This causes stale internal state
(e.g. TaskRunHistory tasks) from the previous issue to persist,
showing execution history from a different issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:58:05 +08:00
Bohan Jiang
73b0fd98d0 Merge pull request #231 from multica-ai/feat/remote-api-env-config
feat(web): support REMOTE_API_URL for remote backend proxy
2026-03-31 16:57:32 +08:00
Bohan Jiang
2706e3d4f5 Merge pull request #229 from multica-ai/agent/j/298fd11b
feat(web): show toast with link after creating an issue
2026-03-31 16:53:55 +08:00
Jiang Bohan
89bedb8f5c feat(web): support REMOTE_API_URL env for proxying to remote backend
- Load root .env in next.config.ts so REMOTE_API_URL is available
- Default fallback remains localhost:8080 (no impact on existing setups)
- Add REMOTE_API_URL to .env.example with documentation
2026-03-31 16:53:32 +08:00
LinYushen
94c62b63d3 Merge pull request #228 from multica-ai/fix/s3-cleanup-on-delete
fix(upload): clean up S3 objects when attachments are deleted
2026-03-31 16:49:47 +08:00
yushen
114d2a3acf Merge remote-tracking branch 'origin/main' into fix/s3-cleanup-on-delete
# Conflicts:
#	server/pkg/db/generated/models.go
2026-03-31 16:49:32 +08:00
Jiang Bohan
da086db982 fix(issues): render comments with Markdown to enable issue mention cards
Comment body was using RichTextEditor in read-only mode which doesn't
support IssueMentionCard rendering. Switch to Markdown component so
mention://issue/ links render as rich cards with status + title.
2026-03-31 16:49:03 +08:00
yushen
79cd2a3a5d fix(upload): link attachments to comments via client-side ID tracking
Instead of regex-parsing markdown content to find attachment URLs
(fragile), the frontend now tracks uploaded attachment IDs and sends
them with the comment creation request. The backend links them by ID.

Frontend: upload returns attachment ID, comment/reply inputs collect
IDs during editing session, pass as attachment_ids on submit.
Backend: CreateComment accepts attachment_ids, links by ID+issue scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:47:27 +08:00
Jiang Bohan
6b2df8a356 feat(web): show toast with link after creating an issue
After creating an issue, a success toast appears showing the issue
identifier (e.g. MUL-72) with a "View issue" action button that
navigates to the issue detail page. Similar to Linear's behavior.
2026-03-31 16:43:26 +08:00
Bohan Jiang
dbd4830e35 Merge pull request #223 from multica-ai/agent/j/36b5c91f
feat(issues): add @ mention for issues in comments
2026-03-31 16:38:12 +08:00
yushen
acba0b8139 fix(upload): clean up S3 objects when attachments are deleted
- Add Delete/DeleteKeys/KeyFromURL methods to S3Storage
- DeleteAttachment handler now removes the S3 object after DB delete
- DeleteComment collects attachment URLs before CASCADE, then cleans S3
- DeleteIssue collects all attachment URLs (issue + comment level) before CASCADE, then cleans S3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:34:47 +08:00
Jiang Bohan
61a34ba5cc merge: resolve conflicts with main and remote 2026-03-31 16:26:27 +08:00
Jiang Bohan
b8c784dda3 merge: resolve conflicts with main
- Take main's router.go, rich-text-editor.tsx, comment-card.tsx
- Remove deleted daemon_pairing.go
- Keep issue mention card feature
2026-03-31 16:25:20 +08:00
Jiang Bohan
34ee700295 fix(editor): post-process mention shortcodes to markdown link format
The Tiptap Mention extension's createInlineMarkdownSpec serializes
mentions as shortcodes [@ id="..." label="..."] — the .extend()
renderMarkdown override may not reliably take effect.

Added a robust fallback: post-process the editor's markdown output
by replacing shortcodes with [@Label](mention://type/id) using the
Tiptap JSON document for type info. Also preprocess stored shortcodes
in the Markdown renderer for backward compatibility.
2026-03-31 16:23:11 +08:00
LinYushen
2c76a0b905 Merge pull request #227 from multica-ai/fix/fetch-credentials-include
fix(api): add credentials include for cross-origin cookie storage
2026-03-31 16:22:54 +08:00
yushen
d57b98fc78 fix(api): add credentials include to fetch for cross-origin cookie storage
The API at multica-api.copilothub.ai sets CloudFront signed cookies
with Domain=.copilothub.ai, but fetch() defaults to credentials:
'same-origin'. Since the frontend (multica-app.copilothub.ai) and API
are cross-origin, the browser silently drops Set-Cookie headers without
credentials: 'include'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:22:25 +08:00
Jiang Bohan
e16f1579a9 feat(issues): render issue mentions as rich cards with status icon
- Fix mention markdown serialization: use renderMarkdown (tiptap/markdown 3.x API)
  instead of addStorage.markdown.serialize which was silently ignored
- Add IssueMentionCard component showing status icon + identifier + title
- Update Markdown renderer to use card for mention://issue/ links
2026-03-31 16:19:32 +08:00
Naiyuan Qing
9e06b02cfa Merge pull request #226 from multica-ai/feature/comment-context-menu
feat(editor): migrate to @tiptap/markdown, TitleEditor, comment UX improvements
2026-03-31 16:19:10 +08:00
Jiang Bohan
e0e52bca64 feat(web): move OK emoji to 2nd position, remove thumbs down 2026-03-31 16:19:03 +08:00
Jiang Bohan
57a5b8b7a4 feat(web): add OK emoji to reaction quick bar 2026-03-31 16:19:02 +08:00
Bohan Jiang
8bbc228469 fix(editor): use correct getMarkdown API for @tiptap/markdown (#219)
The migration from tiptap-markdown to @tiptap/markdown (38e92040)
changed the getMarkdown API location. The old package stored it at
editor.storage.markdown.getMarkdown(), but @tiptap/markdown adds it
directly as editor.getMarkdown(). This caused getEditorMarkdown() to
always return "", preventing description (and any markdown content)
from being saved when creating or editing issues/comments.
2026-03-31 16:19:02 +08:00
Jiayuan
94ddbfb4d9 fix(tests): merge main, renumber migration, fix execenv test assertions
Merge main to pick up 028_task_trigger_comment migration. Renumber
daemon_token migration to 029. Fix execenv tests that expected CLI hints
in issue_context.md after they were moved to CLAUDE.md.
2026-03-31 16:19:02 +08:00
Jiayuan
afdfee78b9 feat(daemon): add authentication for daemon API routes
Issue daemon auth tokens (mdt_) on pairing session claim, bound to
workspace_id + daemon_id with 1-year expiry. Add DaemonAuth middleware
that validates these tokens and falls back to JWT/PAT for backward
compatibility. Apply middleware to all daemon routes except pairing
endpoints.
2026-03-31 16:19:02 +08:00
LinYushen
dc3dec8ebe feat(cli): add multica update command (#218)
* feat(cli): add `multica update` command

Detects whether multica was installed via Homebrew (by resolving the
binary symlink and checking if it lives under a Homebrew prefix).

- Brew installs: runs `brew upgrade multica` automatically.
- Non-brew installs: prints instructions for installing via brew or
  downloading from GitHub releases.
- Checks latest version from the GitHub releases API and skips
  the update if already up to date.

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

* fix(cli): use fully-qualified tap name in brew upgrade

Use `brew upgrade multica-ai/tap/multica` instead of `brew upgrade multica`
to avoid any potential name collision with core formulae.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:19:02 +08:00
Bohan Jiang
9ceea9c17e fix(editor): use correct getMarkdown API for @tiptap/markdown (#217)
The migration from tiptap-markdown to @tiptap/markdown in 38e92040
broke comment creation. The old package stored getMarkdown() on
editor.storage.markdown, but the official @tiptap/markdown extension
adds it directly to the editor instance (editor.getMarkdown()).

This caused getEditorMarkdown() to always return "", making the
submit button permanently disabled and preventing any comments.

Also fix stale submitting ref in useIssueTimeline dependency array.
2026-03-31 16:19:02 +08:00
Bohan Jiang
02918d8229 perf(web): parallelize auth init and non-blocking dashboard layout (#220)
- Fire getMe() and listWorkspaces() in parallel instead of serially,
  saving one network round-trip (~200ms on cloud)
- Render dashboard sidebar shell immediately once user is authenticated,
  show loading indicator in content area while workspace hydrates

Closes MUL-41
2026-03-31 16:19:02 +08:00
Jiayuan
0eaa6e74a4 fix(daemon): update execenv tests to match current renderIssueContext output
CLI hints like "multica issue get" were moved to CLAUDE.md and are no
longer rendered into issue_context.md. Remove stale assertions.
2026-03-31 16:19:01 +08:00
Jiayuan
d4e121284a feat(inbox): add archive button on individual inbox list items
Show an archive icon on hover for each inbox list item, allowing
users to archive a single message directly from the list without
needing to open the detail panel first.
2026-03-31 16:19:01 +08:00
Jiayuan
fc8969a399 fix(issues): remove duplicate commenter header in collapsible comment
The parent comment's header (avatar, name, time, context menu) is now
the collapsible trigger itself. Only the body, reactions, replies, and
reply input collapse — the header is always visible. This removes the
duplicate author info that appeared when expanded.
2026-03-31 16:19:01 +08:00
Jiayuan
90295d8554 fix(daemon): add CLI hint to issue_context.md
renderIssueContext() now includes a "Quick Start" section with the
`multica issue get` command so agents know how to fetch issue details.
Fixes the TestPrepareDirectoryMode and TestWriteContextFiles failures.
2026-03-31 16:19:01 +08:00
Jiayuan
91c279fd2a feat(issues): make entire comment card collapsible with toggle
Each comment card now has a clickable header with a chevron toggle.
When collapsed, shows author, timestamp, and a content preview.
When expanded, shows the full comment body, replies, and reply input.
2026-03-31 16:19:01 +08:00
Jiayuan
ef4e2d94a0 feat(issues): add collapsible toggle for comment replies
Wrap the replies section in a Collapsible component so users can
collapse/expand replies on a comment thread. The parent comment and
reply input remain always visible. A chevron trigger shows the reply
count (e.g. "3 replies") and rotates on open. Default state is expanded
to preserve existing behavior.
2026-03-31 16:19:00 +08:00
Naiyuan Qing
27987adf37 fix(inbox): use issue_id as selection key instead of inbox item id
- URL param: ?id= → ?issue= (keyed by issue, not notification)
- Multiple notifications for same issue now share selection state
- Archive correctly clears selection when archived item's issue matches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:17:41 +08:00
Naiyuan Qing
bb2dd67941 fix(comments): collapsible header overflow, hover style on toggle
- Add shrink-0 to name/time to prevent wrapping when collapsed
- Content preview: min-w-0 flex-1 truncate for proper ellipsis
- Collapsible trigger: add rounded p-0.5 hover:bg-muted for click affordance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:15:26 +08:00
Naiyuan Qing
26af15f189 Merge remote-tracking branch 'origin/main' into feature/comment-context-menu 2026-03-31 16:15:16 +08:00
LinYushen
b5674869ed fix(auth): enforce auth on daemon API routes (#224)
* fix(auth): enforce auth middleware and workspace membership on daemon API routes

Daemon routes were registered without the Auth middleware, meaning the
server accepted unauthenticated requests to register runtimes, claim
tasks, etc. The daemon client already sends a Bearer token — the server
just wasn't validating it.

- Split /api/daemon routes: pairing-session endpoints stay public (used
  before the daemon has a token), all others now require Auth middleware
- Add workspace membership check in DaemonRegister so only workspace
  members can register runtimes
- Update test to include X-User-ID header matching the new auth requirement

Closes MUL-90

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

* refactor(daemon): remove dead pairing-session feature

The daemon pairing flow was never completed — the daemon authenticates
via CLI config token, not pairing sessions. Remove all related code:

- Delete daemon_pairing.go handler (4 unused handlers)
- Remove pairing routes from router.go (3 public + 1 protected)
- Delete /pair/local page + test from frontend
- Remove DaemonPairingSession types and API client methods
- Add migration 029 to drop daemon_pairing_session table
- Update LOCAL_DEVELOPMENT.md to reflect actual auth flow

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:13:58 +08:00
Jiang Bohan
a472a0e8e0 fix(editor): override renderMarkdown/parseMarkdown for mention serialization
The @tiptap/markdown extension discovers serializers via the
renderMarkdown extension field, not addStorage(). The previous
addStorage approach was silently ignored, causing mentions to serialize
as shortcode format [@ id="..." label="..."] instead of markdown links.

Now properly overrides renderMarkdown, parseMarkdown, and
markdownTokenizer to serialize mentions as [@Label](mention://type/id)
which the Markdown renderer can handle as clickable links.
2026-03-31 16:09:31 +08:00
Naiyuan Qing
8fae493f01 merge: resolve conflicts with main (file upload support)
- Merge main's file upload (Image extension, Paperclip, useFileUpload)
- Keep our mention/markdown/TitleEditor changes
- Apply RichTextEditor edit/display to main's Collapsible CommentCard layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:07:45 +08:00
Naiyuan Qing
98829fad29 fix(comments): replace optimistic updates with loading state
- Remove temp-xxx optimistic inserts from submitComment/submitReply
- Wait for API response, then insert real comment into timeline
- Add Loader2 spinner to comment/reply submit buttons during loading
- Remove hover card from Markdown.tsx (will be handled via NodeView later)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:03:13 +08:00
LinYushen
fe0968d96f Merge pull request #213 from multica-ai/feature/file-upload-cloudfront
feat(upload): file upload API with S3 + CloudFront signed cookies
2026-03-31 16:00:19 +08:00
yushen
1e22633301 Merge remote-tracking branch 'origin/main' into feature/file-upload-cloudfront
# Conflicts:
#	apps/web/components/common/rich-text-editor.tsx
#	apps/web/features/issues/components/comment-card.tsx
#	apps/web/package.json
#	pnpm-lock.yaml
2026-03-31 15:59:46 +08:00
yushen
9e23fb76fc fix(upload): harden upload flow — sanitize filenames, refresh CF cookies, deduplicate handlers
- Sanitize Content-Disposition filenames to prevent header injection (strip control chars, quotes, semicolons)
- Add CloudFront cookie refresh middleware so cookies are re-issued when expired
- Log errors in groupAttachments instead of silently swallowing them
- Move useFileUpload hook to shared/hooks/ per project architecture conventions
- Add uploadWithToast helper to deduplicate try/catch/toast pattern across 3 components
- Refactor ApiClient.uploadFile to reuse auth headers, 401 handling, and error parsing
- Allow empty MIME types client-side (let server sniff and decide)
- Constrain Image extension max-width in rich-text-editor to prevent layout overflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:52:40 +08:00
Jiang Bohan
c5b0535a3f fix(markdown): allow mention:// protocol through URL sanitization
react-markdown v10's defaultUrlTransform strips URLs with non-standard
protocols (only http/https/irc/mailto/xmpp allowed). This caused
mention://issue/ links to have empty hrefs, breaking click navigation
to issue detail pages.
2026-03-31 15:52:03 +08:00
Naiyuan Qing
b9ea10c89d fix(comments): unify rendering with RichTextEditor, fix mention/link colors
- Comment display: replace <Markdown> with <RichTextEditor editable={false}>
- Link color: primary → brand (blue)
- Mention color: brand → primary + semibold
- Add MentionHoverCard component with HoverCardTrigger render={<span />}
- Markdown.tsx: sync mention style to text-primary font-semibold

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:50:22 +08:00
Jiayuan Zhang
5a7b34cab5 Merge pull request #211 from multica-ai/agent/lambda/5e70a174
fix(agent): fix agent visibility defaults and permission model
2026-03-31 15:50:07 +08:00
Jiayuan
feb5f57662 fix: merge main and renumber migration 028 → 030
Main now has 028 (task_trigger_comment) and 029 (daemon_token).
Renumber agent_default_private migration to 030 to avoid conflict.
2026-03-31 15:47:09 +08:00
Jiayuan
ac17fd33bf Merge remote-tracking branch 'origin/main' into agent/lambda/5e70a174 2026-03-31 15:45:59 +08:00
Jiayuan Zhang
a2ea8197e2 Merge pull request #221 from multica-ai/agent/lambda/bd1d07a6
feat(server): trigger agents via @mention in comments
2026-03-31 15:44:41 +08:00
Jiayuan
7aea32cb33 fix(server): suppress assignee on_comment when mentions target others
When a comment @mentions anyone but not the assignee agent, the
assignee's on_comment trigger is now suppressed. This prevents the
assignee agent from being re-triggered when users share results with
colleagues or ask other agents for help.

The rule: @mention is an intent signal — if you're talking to someone
else, the assignee agent should not respond.
2026-03-31 15:42:54 +08:00
yushen
f5353c6691 feat(upload): signed URLs for CLI + eager load attachments on comments
- Add CloudFrontSigner.SignedURL() for generating per-resource signed URLs
- Attachment responses include download_url (5-min signed URL for CLI)
- Eager load attachments on comments and timeline (same pattern as reactions)
- Add ListAttachmentsByCommentIDs query for batch loading
- Update Comment and TimelineEntry types with attachments field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:42:10 +08:00
Naiyuan Qing
9f03b73809 feat(editor): add TitleEditor component, replace <input> for issue titles
- New TitleEditor: minimal tiptap (Document+Paragraph+Text+Placeholder)
- Single-paragraph constraint prevents Enter from creating new lines
- contenteditable div enables visual word-wrap (no horizontal scroll)
- Enter→submit+blur, Shift+Enter blocked, Escape→blur
- Replace <Input> in create-issue modal and <input> in issue-detail
- Remove titleDraft state/titleFocusedRef/sync effect from issue-detail
- Fix duplicate React key: TitleEditor key={`title-${id}`}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:38:42 +08:00
Naiyuan Qing
ac2a4c419f refactor(editor): migrate to @tiptap/markdown, fix mention rendering
- Replace tiptap-markdown with official @tiptap/markdown (markdown→JSON direct, skip DOM)
- Add contentType:"markdown" for proper \n\n paragraph parsing
- Fix mention renderHTML: use mergeAttributes for class/data-type, <a>→<span>
- Fix type attribute leak: add renderHTML:()=>({}) to suppress raw "type" attr
- Link style: permanent underline → hover-only underline (matches read-only)
- Mention style: primary+background pill → brand color text only
- Comment edit: replace <input> with RichTextEditor for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:38:29 +08:00
Jiang Bohan
7df140bcda feat(issues): add @ mention for issues in comments
Support mentioning issues via @ in the rich text editor with fuzzy
search on identifier and title. Issue mentions render as clickable
links that navigate to the issue detail page.
2026-03-31 15:38:24 +08:00
Jiayuan Zhang
7b68dd8dda Merge pull request #222 from multica-ai/revert/daemon-auth
revert: daemon authentication for API routes (#214)
2026-03-31 15:33:02 +08:00
Jiayuan
35f77de9cc Revert "Merge pull request #214 from multica-ai/agent/lambda/4771d426"
This reverts commit cfd2fdf70f, reversing
changes made to 987984431b.
2026-03-31 15:30:51 +08:00
Jiayuan
37881adbed feat(server): trigger agents via @mention in comments
When a user @mentions an agent in any issue's comment, the system now
enqueues a task for that agent. The agent reads the issue context and
replies to the triggering comment thread.

Changes:
- Add shared util.ParseMentions for mention parsing (used by both
  comment handler and notification listeners)
- Add EnqueueTaskForMention to TaskService for explicit agent targeting
- Add on_mention trigger type support in agent trigger config
- Add HasPendingTaskForIssueAndAgent SQL query for per-agent dedup
- Add enqueueMentionedAgentTasks in CreateComment handler

Safety: prevents self-trigger (agent mentioning itself), dedup with
assignee on_comment trigger, terminal issue status check, and per-agent
pending task dedup.
2026-03-31 15:30:24 +08:00
yushen
15f96468be feat(upload): add attachment table for tracking uploaded files
- Add attachment table with workspace/issue/comment associations
- Upload handler creates attachment record when workspace context exists
- Add GET /api/issues/{id}/attachments and DELETE /api/attachments/{id}
- Frontend passes issueId context during uploads for tracking
- Add Attachment type, listAttachments, deleteAttachment to API client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:29:41 +08:00
Bohan Jiang
c44da53080 Merge pull request #215 from multica-ai/agent/j/92dbd4ba
feat(web): add OK emoji to reaction quick bar
2026-03-31 15:28:31 +08:00
Bohan Jiang
a5a2673642 fix(editor): use correct getMarkdown API for @tiptap/markdown (#219)
The migration from tiptap-markdown to @tiptap/markdown (38e92040)
changed the getMarkdown API location. The old package stored it at
editor.storage.markdown.getMarkdown(), but @tiptap/markdown adds it
directly as editor.getMarkdown(). This caused getEditorMarkdown() to
always return "", preventing description (and any markdown content)
from being saved when creating or editing issues/comments.
2026-03-31 15:28:02 +08:00
Jiang Bohan
518d4449d7 feat(web): move OK emoji to 2nd position, remove thumbs down 2026-03-31 15:27:13 +08:00
Jiayuan Zhang
cfd2fdf70f Merge pull request #214 from multica-ai/agent/lambda/4771d426
feat(daemon): add authentication for daemon API routes
2026-03-31 15:27:11 +08:00
LinYushen
987984431b feat(cli): add multica update command (#218)
* feat(cli): add `multica update` command

Detects whether multica was installed via Homebrew (by resolving the
binary symlink and checking if it lives under a Homebrew prefix).

- Brew installs: runs `brew upgrade multica` automatically.
- Non-brew installs: prints instructions for installing via brew or
  downloading from GitHub releases.
- Checks latest version from the GitHub releases API and skips
  the update if already up to date.

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

* fix(cli): use fully-qualified tap name in brew upgrade

Use `brew upgrade multica-ai/tap/multica` instead of `brew upgrade multica`
to avoid any potential name collision with core formulae.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:24:05 +08:00
Bohan Jiang
d8a8549c8a fix(editor): use correct getMarkdown API for @tiptap/markdown (#217)
The migration from tiptap-markdown to @tiptap/markdown in 38e92040
broke comment creation. The old package stored getMarkdown() on
editor.storage.markdown, but the official @tiptap/markdown extension
adds it directly to the editor instance (editor.getMarkdown()).

This caused getEditorMarkdown() to always return "", making the
submit button permanently disabled and preventing any comments.

Also fix stale submitting ref in useIssueTimeline dependency array.
2026-03-31 15:23:13 +08:00
Bohan Jiang
461dad0dd5 perf(web): parallelize auth init and non-blocking dashboard layout (#220)
- Fire getMe() and listWorkspaces() in parallel instead of serially,
  saving one network round-trip (~200ms on cloud)
- Render dashboard sidebar shell immediately once user is authenticated,
  show loading indicator in content area while workspace hydrates

Closes MUL-41
2026-03-31 15:22:58 +08:00
yushen
423aa38888 feat(upload): add file upload UI — avatar, editor paste/drop, attachments
- Add uploadFile method to ApiClient (FormData + 401 handling)
- Add useFileUpload hook with client-side validation
- ActorAvatar renders actual avatar images with fallback to initials
- Account settings: replace URL input with clickable avatar upload
- RichTextEditor: add Image extension, paste/drop/insertFile support
- Markdown renderer: add img component for uploaded images
- CommentInput & ReplyInput: add paperclip button for file attachments
- Issue description: paste/drop file upload support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:17:54 +08:00
Jiayuan
497fce0061 fix(tests): merge main, renumber migration, fix execenv test assertions
Merge main to pick up 028_task_trigger_comment migration. Renumber
daemon_token migration to 029. Fix execenv tests that expected CLI hints
in issue_context.md after they were moved to CLAUDE.md.
2026-03-31 15:17:09 +08:00
Jiayuan
9f56f6af81 feat(daemon): add authentication for daemon API routes
Issue daemon auth tokens (mdt_) on pairing session claim, bound to
workspace_id + daemon_id with 1-year expiry. Add DaemonAuth middleware
that validates these tokens and falls back to JWT/PAT for backward
compatibility. Apply middleware to all daemon routes except pairing
endpoints.
2026-03-31 15:17:09 +08:00
Jiayuan Zhang
56b66908a1 Merge pull request #216 from multica-ai/agent/lambda/92e0a175
feat(inbox): support archiving individual messages from list
2026-03-31 15:16:48 +08:00
Jiayuan
1054e218ed fix(daemon): update execenv tests to match current renderIssueContext output
CLI hints like "multica issue get" were moved to CLAUDE.md and are no
longer rendered into issue_context.md. Remove stale assertions.
2026-03-31 15:15:06 +08:00
Jiayuan Zhang
7d9d6793bc Merge pull request #210 from multica-ai/agent/lambda/cbe4d468
feat(issues): add collapsible toggle for comment replies
2026-03-31 15:12:26 +08:00
Jiayuan
9ba12dffd0 fix(issues): remove duplicate commenter header in collapsible comment
The parent comment's header (avatar, name, time, context menu) is now
the collapsible trigger itself. Only the body, reactions, replies, and
reply input collapse — the header is always visible. This removes the
duplicate author info that appeared when expanded.
2026-03-31 15:07:41 +08:00
Jiayuan
8395479653 feat(inbox): add archive button on individual inbox list items
Show an archive icon on hover for each inbox list item, allowing
users to archive a single message directly from the list without
needing to open the detail panel first.
2026-03-31 15:07:37 +08:00
yushen
978a5af5de fix(upload): remove unnecessary uploads/ key prefix
Single-purpose bucket with randomized hex keys doesn't benefit from
a prefix — no lifecycle policies or access controls scoped to it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:01:33 +08:00
yushen
c27b7bab5e fix(upload): sniff content type, sanitize filename, add key prefix
- Use http.DetectContentType() instead of trusting client-declared MIME type
- Sanitize quotes in filename for Content-Disposition header injection
- Add uploads/ prefix to S3 keys for better organization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:58:52 +08:00
yushen
edf4c00c08 fix(upload): add file type/size validation, Content-Disposition header
- Add content type allowlist (images, PDF, text, video, audio, zip)
- Enforce 10 MB upload limit via http.MaxBytesReader
- Set Content-Disposition on S3 objects for proper download filenames
- Remove unused CloudFrontSigner.Domain() method

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:55:27 +08:00
Jiayuan
06424f9ba6 fix(daemon): add CLI hint to issue_context.md
renderIssueContext() now includes a "Quick Start" section with the
`multica issue get` command so agents know how to fetch issue details.
Fixes the TestPrepareDirectoryMode and TestWriteContextFiles failures.
2026-03-31 14:53:05 +08:00
Jiang Bohan
26064e43d1 feat(web): add OK emoji to reaction quick bar 2026-03-31 14:52:30 +08:00
Naiyuan Qing
3b6f64ba8e Merge pull request #212 from multica-ai/feature/comment-context-menu
feat(issues): comment context menu & mention popup positioning fix
2026-03-31 14:46:07 +08:00
Naiyuan Qing
38e92040c4 fix(editor): migrate tiptap-markdown import to @tiptap/markdown
Update import path and remove deprecated config options (html,
transformPastedText, transformCopiedText) that don't exist in the
official @tiptap/markdown package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:44:00 +08:00
Jiayuan
d729f9c5be feat(issues): make entire comment card collapsible with toggle
Each comment card now has a clickable header with a chevron toggle.
When collapsed, shows author, timestamp, and a content preview.
When expanded, shows the full comment body, replies, and reply input.
2026-03-31 14:41:44 +08:00
yushen
29a80e057e feat(upload): add file upload API with S3 + CloudFront signed cookies
Add POST /api/upload-file endpoint that uploads files to S3 and returns
CDN URLs protected by CloudFront signed cookies (same pattern as Linear).

Infrastructure:
- Two private S3 buckets (static.multica.ai, static-staging.multica.ai)
- Two CloudFront distributions with OAC and Trusted Key Groups
- ACM wildcard cert in us-east-1, DNS records in Route 53
- RSA signing key stored in AWS Secrets Manager

Backend:
- S3 storage service with CloudFront CDN domain support
- CloudFront signed cookie generation (RSA-SHA1)
- Private key loaded from Secrets Manager (env var fallback for local dev)
- Cookies set on login (VerifyCode) with 72h expiry matching JWT
- Upload handler: multipart form → S3 → CloudFront URL response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:41:17 +08:00
Jiayuan
f69aa93a75 feat(agents): add Settings tab for editing agent visibility and properties
Adds a Settings tab to the agent detail panel with:
- Name and description editing
- Visibility toggle (workspace/private) matching the create dialog pattern
- Max concurrent tasks configuration
- Runtime info display (read-only)
2026-03-31 14:40:53 +08:00
Naiyuan Qing
b4bbc16521 fix(issues): use floating-ui for mention popup viewport-aware positioning
Replace hardcoded bottom positioning with @floating-ui/dom computePosition
so the @ mention popup flips above the cursor when near the viewport bottom.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:39:48 +08:00
Jiayuan
af94e22cba merge: resolve conflict with main (comment context menu icons) 2026-03-31 14:37:35 +08:00
Jiayuan
bedf4a05c8 fix(agent): fix agent visibility defaults and permission model
- Change DB default for agent visibility from 'workspace' to 'private'
- Fix canManageAgent: workspace agents are now manageable by all members,
  private agents remain restricted to owner/admin
- Add private agent visibility check to BatchAssigneePicker (was missing)
2026-03-31 14:34:04 +08:00
Jiayuan
7dafb5127d feat(issues): add collapsible toggle for comment replies
Wrap the replies section in a Collapsible component so users can
collapse/expand replies on a comment thread. The parent comment and
reply input remain always visible. A chevron trigger shows the reply
count (e.g. "3 replies") and rotates on open. Default state is expanded
to preserve existing behavior.
2026-03-31 14:33:09 +08:00
Naiyuan Qing
8fa4c8f576 Merge pull request #208 from multica-ai/feature/comment-context-menu
feat(issues): add context menu to all comments with copy support
2026-03-31 14:21:35 +08:00
Naiyuan Qing
d70ab81363 feat(issues): add context menu to all comments with copy support
Own comments show Copy/Edit/Delete; others' comments show Copy only.
Also adds icons to menu items for consistency with other dropdown menus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:15:09 +08:00
LinYushen
961de18c97 feat(agents): reply as thread instead of top-level comment (#205)
* feat(agents): reply as thread instead of top-level comment

When an agent responds to a user comment, the reply is now nested under
the triggering comment (parent_id) instead of appearing as a separate
top-level comment. Also enables on_comment trigger by default for newly
created agents.

- Add trigger_comment_id column to agent_task_queue (migration 028)
- Pass triggering comment ID through EnqueueTaskForIssue → task → createAgentComment
- Include parent_id in WebSocket broadcast for agent comments
- Default agent creation includes both on_assign and on_comment triggers

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

* feat(cli): add --parent flag to comment add for threaded replies

The agent posts comments via the CLI, so the correct fix is giving it a
--parent flag rather than wiring trigger_comment_id through the task
infrastructure. The agent reads the comment list, decides which comment
to reply to, and passes --parent <comment-id>.

- Add --parent flag to `multica issue comment add`
- Update agent runtime instructions to explain --parent usage

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

* feat(daemon): pass trigger_comment_id to agent execution context

The agent now knows which comment triggered its task and gets an explicit
instruction to reply to it using --parent. The trigger_comment_id flows
from the DB through the claim response, daemon Task struct, and into
issue_context.md where the agent sees it.

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

* fix(comments): agent replies to thread root, matching frontend behavior

When the triggering comment is itself a reply (has parent_id), resolve
to the thread root so the agent's reply stays in the same flat thread.
This matches the frontend where all replies share the top-level parent.

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

* feat(cli): show parent_id and full IDs in comment list

The table output now includes a PARENT column and shows full comment IDs
(not truncated) so agents can see thread structure and use --parent.

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

* feat(daemon): instruct agents to always use --output json

Agents now see explicit guidance to use --output json for all read
commands, ensuring they get structured data with full IDs and parent_id
for proper threading.

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

* feat(daemon): differentiate comment-trigger vs assign-trigger context

When triggered by a comment, the agent now gets clear instructions:
- Primary goal is to read and respond to the comment
- Do NOT change issue status just because you replied
- Only change status if explicitly requested

This prevents the agent from seeing "In Review" and stopping, since
it now understands the task is to reply, not to re-evaluate the issue.

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

* fix(daemon): split workflow by trigger type in CLAUDE.md/AGENTS.md

The Workflow section in the agent's runtime config now shows a
comment-reply workflow when triggered by a comment (read comments,
find trigger, reply, don't change status) vs the full assignment
workflow (set in_progress, do work, set in_review).

Previously the agent always saw the assignment workflow, causing it
to check the issue status, see "In Review", and stop without reading
or replying to the triggering comment.

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

* refactor(daemon): remove duplicate workflow from issue_context.md

Workflow instructions now live only in CLAUDE.md/AGENTS.md (runtime_config.go).
issue_context.md keeps just the task data: issue ID, trigger type, and
triggering comment ID.

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

* fix(task): skip duplicate comment on completion for comment-triggered tasks

When triggered by a comment, the agent posts its own reply via CLI
with --parent. The task completion path was also creating a comment
from the agent's stdout output, resulting in duplicates. Now only
assignment-triggered tasks auto-post output as a comment. Error
messages from FailTask are still posted regardless of trigger type.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:48:39 +08:00
Naiyuan Qing
5517136d73 Merge pull request #207 from multica-ai/feature/ws-self-event-filtering
feat(realtime): WS self-event filtering, issue-detail refactor, sync gap fixes
2026-03-31 13:19:39 +08:00
Naiyuan Qing
6761310038 fix(sync): board-card rollback, inbox status sync, markRead error handling
- board-card: capture prev issue before optimistic update, restore on error
- useRealtimeSync: wire issue:updated WS handler to update inbox issue_status
- inbox: markRead uses optimistic update, refetch on error with toast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:09:17 +08:00
Naiyuan Qing
a2e5cbd47b refactor(issues): extract hooks from issue-detail, eliminate dual source of truth
- Remove useState<Issue> mirror anti-pattern — read directly from useIssueStore
- handleUpdateField now writes to global store (board/list sync instantly)
- handleDelete now calls removeIssue (deleted issue disappears from list)
- Extract useIssueTimeline hook (comment CRUD + WS events + reconnect)
- Extract useIssueReactions hook (issue reactions + WS events)
- Extract useIssueSubscribers hook (subscribers + WS events + rollback)
- Add useWSReconnect hook for per-component reconnect handling
- Add React.memo to BoardCardContent, DraggableBoardCard, ListRow
- Add key={id} to RichTextEditor to fix stale description on issue switch
- issue-detail.tsx: 1330 → 979 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:09:17 +08:00
Naiyuan Qing
3e8c715de8 feat(realtime): skip WS refetch for self-triggered events
Backend WS messages now include actor_id from the Event struct.
Frontend useRealtimeSync skips the debounced refetch when the event
was triggered by the current user, eliminating unnecessary re-renders
of heavy components (~400ms after each user action).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:09:17 +08:00
Jiayuan Zhang
c1f81427f6 Merge pull request #206 from multica-ai/agent/lambda/6e8277a7
feat(web): add My Issues page with grouped filters
2026-03-31 12:57:02 +08:00
Jiayuan
d199b6c728 feat(web): add My Issues page with grouped filters
Add a new "My Issues" page accessible from the sidebar, positioned
between Inbox and Issues. Issues are grouped into three collapsible
sections: assigned to me, assigned to my agents, and created by me.
2026-03-31 12:53:21 +08:00
Jiayuan Zhang
d183e61653 Merge pull request #204 from multica-ai/agent/lambda/b3857b5a
fix(inbox): skip inbox notification for task:completed events
2026-03-31 12:41:21 +08:00
Jiayuan
43fceb9117 fix(inbox): skip inbox notification for task:completed events
Task completion is already visible from the issue status change,
so a separate inbox notification is unnecessary noise.
2026-03-31 12:26:26 +08:00
Naiyuan Qing
2152fec4ee Merge pull request #203 from multica-ai/forrestchang/issue-filter-assignee-creator
feat(issues): add assignee and creator filters
2026-03-31 11:06:16 +08:00
Naiyuan Qing
0cc9c213b5 feat(issues): add assignee and creator filters with two-level dropdown
Add Assignee and Creator filter categories to the issue board filter menu,
using DropdownMenu sub-menus with hover-visible checkboxes (shadcn official
data-selected pattern from PR #6862).

Key changes:
- view-store: add assigneeFilters, includeNoAssignee, creatorFilters state
  with positive selection model (empty = no filter, selected = show only matching)
- issues-header: two-level DropdownMenu — category list → sub-menu with
  searchable checkbox items, issue counts, avatar grouping (Members/Agents)
- utils/filter: extract shared filterIssues() to eliminate duplication
  between issues-page and issues-header
- Workspace switch clears actor filters via deferred subscription
  (dynamic import to avoid circular dependency)
- 10 new filter behavior tests covering assignee, creator, no-assignee,
  and combined filter scenarios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:04:21 +08:00
Naiyuan Qing
1abd3ee918 chore(ui): switch shadcn menuColor from inverted-translucent to default
Replace semi-transparent glassmorphism menus with solid popover backgrounds.
Re-downloaded dropdown-menu, context-menu, select, combobox, menubar, and
button via `npx shadcn add --overwrite`. Also removed `focus:**:text-accent-foreground`
from DropdownMenuCheckboxItem to prevent color cascade into nested components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:01:54 +08:00
Jiayuan Zhang
b9fdaf62ac Merge pull request #202 from multica-ai/forrestchang/issue-status-style
feat(ui): restyle issue status and priority with colored badges
2026-03-31 03:28:25 +08:00
Jiayuan
8a61c94b98 feat(ui): restyle issue status and priority with colored badges
- Status labels use colored pill badges (solid bg for active, muted for inactive)
- Board columns have tinted backgrounds matching their status color
- Priority badges use orange (--priority) design token for clear distinction from status
- Issue cards restructured: identifier, title, then assignee/priority/date row
- Agent avatar default color changed from blue to gray
- New Issue button in header changed to solid/primary style
- Reduced hover shadow on board cards
- Added inheritColor prop to StatusIcon and PriorityIcon for badge use
2026-03-31 03:26:43 +08:00
Jiayuan Zhang
32e19f847f Merge pull request #201 from multica-ai/forrestchang/agent-live-output
feat(agent): stream live agent output and execution history
2026-03-30 23:51:07 +08:00
Jiayuan
a00485cf13 feat(security): redact sensitive information in agent live output
Server-side (primary): Apply redact.Text/InputMap on task message
content, output, and input fields before DB persistence and WebSocket
broadcast. Extended redact package with GitLab tokens, JWTs, connection
strings, and PASSWORD/SECRET/TOKEN env var patterns.

Frontend (fallback): redactSecrets utility mirrors server patterns,
applied in buildTimeline and ToolCallRow render as a safety net.
2026-03-30 23:38:49 +08:00
Jiayuan
e20e1b74dc merge: resolve conflicts with main (reactions feature)
Main added reaction routes and event types while this branch added
task message routes and event types. Both sides kept — no code lost.
2026-03-30 23:29:42 +08:00
Jiayuan
83f325c586 fix(ui): apply mono font directly to code/pre elements
Browser UA stylesheet sets font-family: monospace on <code> and <pre>,
overriding the inherited Geist Mono from parent containers. Apply
font-mono explicitly on these elements so they use the project's
monospace font instead of the browser default.
2026-03-30 23:27:09 +08:00
Jiayuan
1e2052c689 feat(agent): improve live output UI and add execution history
- Fix duplicate icons in tool call rows (use chevron only for expand/collapse)
- Show detailed tool information (WebSearch queries, Agent prompts, Skill names)
- Add thinking/reasoning rows with Brain icon and expandable content
- Show tool results as separate chronological entries with previews
- Add TaskRunHistory component for viewing past agent execution logs
- Add listTasksByIssue API endpoint and task-runs route
- Support thinking content blocks in agent SDK (MessageThinking type)
- Improve callID→toolName mapping in daemon message forwarding
2026-03-30 23:10:54 +08:00
Jiayuan
3c93ebaf1c feat(agent): stream live agent output to issue detail page
When an agent is working on an issue, users can now see real-time output
in the issue detail page instead of waiting for completion.

Backend:
- Add task_message table and migration for persisting agent messages
- Add POST /api/daemon/tasks/{id}/messages endpoint for daemon to report
  structured messages (tool_use, tool_result, text, error) in batches
- Add GET /api/daemon/tasks/{id}/messages for catch-up after reconnect
- Add GET /api/issues/{id}/active-task to check for running tasks
- Broadcast task:message events via WebSocket
- Daemon forwards agent session messages with 500ms text throttling

Frontend:
- Add AgentLiveCard component showing live tool calls, text output,
  and progress indicators with auto-scroll
- Wire into issue detail timeline with WS subscription and HTTP catch-up
- Card appears when agent is working, disappears on completion/failure
2026-03-30 22:53:28 +08:00
Jiayuan Zhang
a526b96b6c Merge pull request #200 from multica-ai/forrestchang/comment-reactions
feat(reactions): emoji reactions for comments and issues
2026-03-30 22:40:54 +08:00
Jiayuan
7c1aabbe3a feat(reactions): add emoji reactions for comments and issue descriptions
Add Slack-style emoji reactions to comments and issue descriptions with
full-stack support: database tables, REST API endpoints, real-time
WebSocket sync, optimistic UI updates, and inbox notifications.

- New `comment_reaction` and `issue_reaction` tables with migrations
- POST/DELETE endpoints for adding/removing reactions on both comments
  and issue descriptions
- Real-time WS events (reaction:added/removed, issue_reaction:added/removed)
- Shared ReactionBar component with quick emoji picker and full emoji-mart
  picker (lazy-loaded)
- Optimistic add/remove with rollback on failure
- Inbox notifications for comment author and issue creator when reacted to
- Reactions included in timeline, comment list, and issue detail responses
2026-03-30 22:37:59 +08:00
Jiayuan Zhang
e07b121fa8 Merge pull request #198 from multica-ai/forrestchang/agent-sandbox-security
feat(security): agent output redaction and private agent visibility
2026-03-30 22:30:27 +08:00
Jiayuan
ad697e0029 feat(security): restrict agent management to owner for private agents
Replace blanket owner/admin role check in UpdateAgent and DeleteAgent
with canManageAgent, which requires the requesting user to be the
agent's owner (or a workspace owner/admin) for private agents.
2026-03-30 22:27:19 +08:00
Jiayuan
0491350f1b feat(security): add agent output redaction and private agent assignment enforcement
- Add redact package to detect and mask secrets (AWS keys, private keys,
  API tokens, bearer tokens, credentials, home paths) in agent output
  before posting as comments in TaskService
- Enforce agent visibility on issue assignment: private agents can only
  be assigned by their owner or workspace admins
- Add visibility picker (workspace/private) to CreateAgentDialog,
  default to private
- Grey out unassignable private agents in the assignee picker with
  lock icon indicator
2026-03-30 22:22:04 +08:00
Jiayuan Zhang
72e3ccfe33 Merge pull request #197 from multica-ai/forrestchang/daemon-profile
feat(daemon): add --profile flag for multi-environment isolation
2026-03-30 20:24:03 +08:00
Jiayuan
8fa1b163a6 feat(daemon): add --profile flag for multi-environment isolation
Allow running multiple daemon instances against different servers (e.g.
production and local dev) simultaneously. Each profile gets isolated
config, PID file, log file, health port, and workspaces root.

Usage:
  multica login --profile dev --server-url http://localhost:8080
  multica daemon start --profile dev

Default profile (no --profile flag) behavior is unchanged.

Closes MUL-42
2026-03-30 20:21:23 +08:00
Jiayuan Zhang
564eb55f90 Merge pull request #196 from multica-ai/forrestchang/task-workspace-dir
feat(daemon): group task directories by workspace ID
2026-03-30 20:15:34 +08:00
Jiayuan
7d126cc549 feat(daemon): group task directories by workspace ID
Task execution environments were all created flat under WorkspacesRoot,
mixing tasks from different workspaces. Now tasks are nested under their
workspace ID for clearer organization and easier per-workspace cleanup.
2026-03-30 20:13:30 +08:00
Jiayuan Zhang
9c3ff52363 Merge pull request #194 from multica-ai/fix/daemon-auto-discover-new-workspaces
fix(daemon): auto-discover new workspaces without restart
2026-03-30 19:34:04 +08:00
Jiayuan
219b174819 fix(daemon): sync workspaces immediately on startup
Run syncWorkspacesFromAPI once before entering the periodic ticker
loop so newly created workspaces are discovered without the initial
30-second delay.
2026-03-30 19:32:24 +08:00
Jiayuan Zhang
e27afce90a Merge pull request #195 from multica-ai/mul-39/remove-default-shortcuts
Remove default keyboard shortcuts
2026-03-30 18:48:51 +08:00
Jiayuan
b2e800b605 refactor(web): remove default keyboard shortcuts
Remove the sidebar toggle shortcut (Ctrl/Cmd+B) and theme toggle
hotkey (D key) global keyboard listeners.
2026-03-30 18:47:14 +08:00
Jiayuan
25ed043117 fix(daemon): auto-discover new workspaces without restart
The daemon now periodically fetches the user's workspace list from the
API (every 30s) and adds any new workspaces to the watched config. The
existing config-watch loop then picks up the change and registers
runtimes. This fixes the issue where workspaces created after
`multica login` were not discovered until the daemon was restarted.
2026-03-30 18:08:58 +08:00
LinYushen
457a3eb555 feat(agents): add on_comment trigger option to agent triggers UI (#193)
The backend already supports on_comment triggers but the frontend was
missing the UI to configure them. Adds the "On Comment" trigger type
and "Add On Comment" button alongside the existing On Assign and
Scheduled options.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:59:55 +08:00
Jiayuan Zhang
3baf141265 Merge pull request #192 from multica-ai/forrestchang/runtime-default-name
feat(daemon): include device name in runtime display name
2026-03-30 17:56:43 +08:00
Jiayuan
7aa784ccd3 feat(daemon): include device name in runtime display name
Change runtime naming from "Local Claude" to "Claude (hostname)" so
team members can distinguish runtimes in shared workspaces.
2026-03-30 17:54:29 +08:00
Naiyuan Qing
11954dd7a7 Merge pull request #191 from multica-ai/fix/tiptap-link-navigation
fix(editor): enable link navigation and fix copy artifacts
2026-03-30 17:51:27 +08:00
Naiyuan Qing
81b518d0d1 fix(editor): enable link navigation and fix <url> copy artifacts
- Set openOnClick: true so clicking a link opens it in a new tab
- Add Cmd+Click / Ctrl+Click handler as fallback (skips mention:// links)
- Override prosemirror-markdown link serializer to always use [text](url)
  format instead of <url> autolink syntax, fixing angle brackets appearing
  when copying links from the editor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:45:20 +08:00
Naiyuan Qing
0a38643ef4 Merge pull request #190 from multica-ai/fix/inbox-selection-after-refresh
fix(inbox): fix inbox items unclickable after page refresh
2026-03-30 17:27:23 +08:00
Naiyuan Qing
b4e8dc3769 fix(inbox): use history.replaceState for selection to fix post-refresh clicks
router.replace() triggers a full server navigation cycle in Next.js 15+,
which can stall after a page refresh (no client route cache), preventing
useSearchParams from updating and making inbox items unclickable.

window.history.replaceState() updates the URL synchronously without
triggering server navigation, which is the recommended approach for
URL state management in Next.js 14.1+.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:25:17 +08:00
Naiyuan Qing
1a98cac165 Merge pull request #189 from multica-ai/NevilleQingNY/workspace-isolation-and-agent-parity
feat(api): workspace isolation + agent parity + timeline UI fixes
2026-03-30 17:11:10 +08:00
Naiyuan Qing
0c4cef0ff1 fix(issues): improve timeline activity entry alignment and overflow
- Add items-center for vertical centering between icons and text
- Add truncate on activity text to prevent line wrapping
- Unify icon/avatar sizes to 16px for visual consistency
- Remove connector line (will revisit later)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:09:21 +08:00
Naiyuan Qing
9da4f0d68a Merge pull request #188 from multica-ai/NevilleQingNY/workspace-isolation-and-agent-parity
feat(api): strict workspace isolation + agent parity fixes
2026-03-30 16:51:41 +08:00
Naiyuan Qing
9ede795c5b feat(api): strict workspace isolation + agent parity fixes
Enforce workspace isolation at every layer:

- Router: move RequireWorkspaceMember middleware to group level so ALL
  workspace-scoped routes (issues, agents, skills, runtimes, inbox,
  comments) require workspace context
- SQL: add GetXxxInWorkspace queries that filter by workspace_id,
  eliminating cross-workspace data access at the query level
- Handlers: loadXForUser functions use workspace-scoped queries,
  no fallback to unscoped queries
- Migration 025: add workspace_id column to comment table with backfill
- ListComments: add workspace_id filter for defense-in-depth

Fix daemon workspace mapping:
- Server returns workspace_id in task claim response (from issue)
- Daemon uses task.WorkspaceID directly instead of unreliable
  workspaceIDForRuntime() local map lookup
- Remove workspaceIDForRuntime function

Fix agent/human parity:
- Comment update/delete: use resolveActor for isAuthor check so agents
  can edit/delete their own comments
- Event attribution: replace hardcoded "member" with resolveActor in
  agent, skill, and subscriber publish calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:49:13 +08:00
LinYushen
16e0645c75 fix(cli): daemon reads server_url from config file (#187)
* fix(cli): daemon reads server_url from config file

The daemon only checked CLI flags and env vars for server_url, ignoring
the persisted config in ~/.multica/config.json. Now falls back to the
config file when neither flag nor env var is set.

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

* fix(cli): reset workspace data when server_url changes on login

When logging in to a different server, stale workspace_id and
watched_workspaces from the previous server caused 404 errors in the
daemon. Now both browser and token login paths clear workspace data
when the server URL changes.

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

* fix(cli): reset workspace data on every login, not just server change

A different user logging in on the same machine would inherit stale
workspace data. Always clear workspace_id and watched_workspaces on
login so autoWatchWorkspaces repopulates them fresh.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:23:31 +08:00
LinYushen
40aa3f6bd9 feat(cli): support app_url in CLI config (#186)
* feat(cli): support app_url in CLI config for browser login

The login flow opens the frontend URL for browser-based auth, but
previously app_url could only be set via environment variables.
Add app_url to CLIConfig so it can be persisted with `multica config set`.

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

* fix(cli): persist app_url to config during browser login

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:51:17 +08:00
yushen
ce43ff014d chore: add Dockerfile for Go backend
Multi-stage build that compiles server, CLI, and migrate binaries,
then produces a minimal Alpine runtime image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:41:42 +08:00
Naiyuan Qing
038dac7fcb Merge pull request #185 from multica-ai/NevilleQingNY/issue-draft-persist
feat(issues): persist create-issue draft with sidebar indicator
2026-03-30 15:00:09 +08:00
Naiyuan Qing
bf379b2e76 feat(issues): persist create-issue draft with sidebar indicator
- Add zustand draft store with localStorage persistence
- Restore draft fields when reopening create-issue modal
- Clear draft only on successful submission
- Show brand-colored dot on sidebar new-issue button when draft exists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:57:29 +08:00
Naiyuan Qing
c27715dd5c Merge pull request #184 from multica-ai/NevilleQingNY/ui-fixes-batch
fix(ui): batch UI fixes — spacing, comments, activity coalescing
2026-03-30 14:31:38 +08:00
Naiyuan Qing
168d9ab5a2 fix(issues): add consistent spacing between list header and items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:29:29 +08:00
Naiyuan Qing
632ff1f8ae fix(issues): update test mocks and workspace switch ordering
- Add stable issue store mock to prevent infinite effect loop in tests
- Update expected error message in not-found test case
- Clear runtime store on workspace switch and set workspace before hydration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:22:43 +08:00
Naiyuan Qing
cd34b82454 fix(issues): UI polish batch — comment input, card gap, board title, activity coalescing
- CommentInput: remove border-t divider, position submit button inside
  editor area (bottom-right) for cleaner look
- CommentCard: add !gap-0 to override Card's default gap-4
- CommentInput/ReplyInput: strip trailing empty lines from markdown
  before submit to prevent extra blank lines in rendered comments
- BoardCard: use normal text color for title instead of muted+hover
- Timeline: coalesce same actor + same action within 2 min window,
  keeping only the final result (e.g. 5 status changes → 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:22:43 +08:00
Naiyuan Qing
9e2cb4040f fix(agents): show error toast on agent creation failure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:22:43 +08:00
Naiyuan Qing
cc19cfb15a fix(issues): unify activity/comment timeline spacing with flex-col gap
Replace mixed spacing approach (space-y + pb-3) with consistent
flex-col gap-3. Activity connector lines now use absolute positioning
to bridge between icons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:22:43 +08:00
Jiayuan Zhang
f9b1be0e39 Merge pull request #183 from multica-ai/forrestchang/batch-issue-ops
feat(issues): batch operations in list view
2026-03-30 13:57:49 +08:00
Jiayuan
781c3d10e8 merge: resolve conflict with RequireWorkspaceMember middleware
Main added RequireWorkspaceMember middleware to issue list routes.
Applied the same middleware to the new batch-update and batch-delete
routes.
2026-03-30 13:56:07 +08:00
Jiayuan
10f88c5fcd feat(issues): add batch operations for issue list view
Add multi-select with batch update/delete support in the list view.
Users can select issues via checkboxes (per-row or per-status-group)
and apply bulk status, priority, assignee changes or delete via a
floating toolbar.
2026-03-30 13:52:59 +08:00
Jiayuan Zhang
681da9d669 Merge pull request #180 from multica-ai/agent/lambda/6f41715a
refactor(server): consolidate workspace permission checks into middleware
2026-03-30 13:44:11 +08:00
Jiayuan Zhang
3f2b345a1d Merge pull request #182 from multica-ai/agent/lambda/532f6d3d
fix(handler): set issue_prefix when auto-creating workspace on first login
2026-03-30 13:40:14 +08:00
Jiayuan
f5519e4f64 fix(handler): set issue_prefix when auto-creating workspace on first login
ensureUserWorkspace in auth.go omitted the IssuePrefix field when
creating a default workspace during first login. This caused the
workspace's issue_prefix to be empty, producing identifiers like "-16"
instead of "JIA-16".

- Pass generateIssuePrefix(wsName) when creating the default workspace
- Add fallback in getIssuePrefix to regenerate from workspace name if
  the stored prefix is empty
- Add migration 024 to backfill empty prefixes on existing workspaces
2026-03-30 13:26:42 +08:00
LinYushen
d41b986cb0 feat(server): distinguish agent vs human CLI actions (#181)
* feat(server): distinguish agent vs human CLI actions via X-Agent-ID/X-Task-ID headers

Extract resolveActor helper in handler to centralize agent identity resolution
from X-Agent-ID header with X-Task-ID cross-validation. Fix DeleteComment,
DeleteIssue, and UpdateComment handlers that previously hardcoded "member" as
actor type. Forward MULTICA_TASK_ID as X-Task-ID header from CLI client.

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

* fix(server): add debug logging and test coverage for resolveActor

Add slog.Debug on agent/task validation failures for easier debugging.
Add TestResolveActor with 5 cases covering member fallback, valid agent,
non-existent agent, valid task, and mismatched task.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:12:59 +08:00
Jiayuan Zhang
810f2df8be Merge pull request #179 from multica-ai/forrestchang/daemon-log-readability
fix(daemon): improve agent execution log readability
2026-03-30 03:48:03 +08:00
Jiayuan
f4a6e7c475 refactor(server): consolidate workspace permission checks into middleware
Move workspace membership and role validation from individual handlers
into dedicated Chi middleware. The new middleware resolves workspace ID
(from query param, X-Workspace-ID header, or URL param), validates
membership via DB, and injects the member into request context.

Handlers now read workspace ID and member from context instead of
calling requireWorkspaceMember/requireWorkspaceRole directly. This
eliminates ~17 duplicated permission checks across handlers and makes
it harder to accidentally omit access control on new routes.
2026-03-30 03:40:20 +08:00
Jiayuan
9b2ded6720 fix(daemon): improve agent execution log readability
Use short task IDs (8-char prefix), task-scoped loggers for concurrent
task disambiguation, numbered tool-use at INFO level for progress
tracking, and an agent-finished summary with duration and tool count.
2026-03-30 03:40:02 +08:00
Jiayuan Zhang
c2af66411d Merge pull request #177 from multica-ai/forrestchang/task-queue-readability
fix(web): improve agent task queue readability
2026-03-30 03:33:48 +08:00
Jiayuan
ed56b85d2a fix(web): improve agent task queue readability
Show issue identifier and title instead of truncated UUIDs, sort active
tasks to top, highlight running/dispatched tasks, and show contextual
timestamps.
2026-03-30 03:29:54 +08:00
Jiayuan Zhang
e1e4079da1 Merge pull request #176 from multica-ai/forrestchang/daemon-stuck-task
fix(daemon): prevent stuck tasks from blocking queue, add concurrency
2026-03-30 03:10:35 +08:00
Jiayuan
67f1f49b09 fix(daemon): prevent stuck tasks from blocking queue and add concurrent execution
- Expand FailAgentTask SQL to accept dispatched OR running status
- Add FailStaleTasks server-side sweeper (dispatched >5min, running >2.5h)
- Fix daemon handleTask to fail tasks on all error paths (StartTask, CompleteTask)
- Make daemon poll loop concurrent with semaphore (default 20 parallel tasks)
- Raise default agent max_concurrent_tasks from 1 to 6 (migration 023)
- Add --max-concurrent-tasks CLI flag and MULTICA_DAEMON_MAX_CONCURRENT_TASKS env
2026-03-30 03:08:52 +08:00
Jiayuan Zhang
00c54232ad Merge pull request #174 from multica-ai/forrestchang/inbox-hide-sidebar
feat(inbox): hide issue detail sidebar by default
2026-03-30 02:46:51 +08:00
Jiayuan Zhang
cbf8f95d05 Merge pull request #175 from multica-ai/forrestchang/agent-comment-identity
fix(handler): attribute agent CLI actions to agent identity
2026-03-30 02:46:38 +08:00
Jiayuan
a4c8bbb03c fix(handler): attribute agent CLI actions to agent identity
When agents use the multica CLI during task execution, their comments,
issue updates, and issue creations were attributed to the daemon's user
(via JWT) instead of the agent. Pass MULTICA_AGENT_ID env var from the
daemon, send X-Agent-ID header from the CLI client, and use it in
handlers to set the correct author/actor identity.
2026-03-30 02:41:51 +08:00
Jiayuan
63fd7efc95 feat(inbox): hide issue detail sidebar by default
The properties sidebar in the inbox view is not needed by default.
Add defaultSidebarOpen and layoutId props to IssueDetail so inbox
can start with the sidebar collapsed and persist its layout separately.
2026-03-30 02:41:41 +08:00
Jiayuan Zhang
663dec52b8 Merge pull request #173 from multica-ai/forrestchang/fix-cli-login-token
fix(cli): always update server_url on login
2026-03-30 01:51:33 +08:00
Jiayuan
c3a9190016 fix(cli): always update server_url on login
Previously, `multica login` only saved server_url to config when it was
empty. A stale value (e.g. from a prior session pointing at a different
host) was never overwritten, causing subsequent API calls to hit the
wrong server.
2026-03-30 01:49:07 +08:00
Jiayuan Zhang
a10515fa74 Merge pull request #172 from multica-ai/forrestchang/agent-repo-context
feat(daemon): agent-driven repo checkout with bare clone cache
2026-03-29 19:44:27 +08:00
Jiayuan
99ada27925 merge: resolve conflicts with main (workdir reuse)
Main added execenv.Reuse() for workdir reuse across tasks on the same
issue. Our branch removed Type/BranchName/gitRoot from Environment
(repos are now checked out on demand). Resolution: keep Reuse() but
simplify it to work with the new Environment struct (no workspace type
tracking). Keep the "reused" log field from main, drop removed fields.
2026-03-29 19:42:51 +08:00
Jiayuan
cdc1ac708e feat(daemon): agent-driven repo checkout with bare clone cache
Agents now decide which repo to use based on issue context and check out
repos on demand via `multica repo checkout <url>`. Workspace repos are
cached locally as bare clones for fast worktree creation.

Key changes:
- Add repocache package for bare clone management (clone, fetch, worktree)
- Add `multica repo checkout` CLI command that talks to local daemon
- Add POST /repo/checkout endpoint on daemon health server
- Pass workspace repos metadata through register + task claim responses
- Remove pre-created worktrees from execenv (workdir starts empty)
- Update CLAUDE.md template to instruct agents to use `multica repo checkout`
- Pass MULTICA_DAEMON_PORT, WORKSPACE_ID, AGENT_NAME, TASK_ID env vars to agent
2026-03-29 19:37:48 +08:00
Jiayuan Zhang
3b58dff375 Merge pull request #171 from multica-ai/forrestchang/issue-resume-qa
feat(daemon): reuse workdir across tasks on same agent+issue
2026-03-29 18:42:11 +08:00
Jiayuan
ce2b263ea5 feat(daemon): reuse workdir across tasks on same agent+issue pair
Previously each task created a fresh workdir via execenv.Prepare(), even
when resuming work on the same (agent, issue). This caused the agent's
session context to be out of sync with a blank code state.

Now the server returns prior_work_dir in the claim response, and the
daemon tries execenv.Reuse() first — which wraps the existing directory,
detects git worktree state, and refreshes context files. Falls back to
Prepare() if the prior workdir no longer exists. Workdirs are no longer
cleaned up after task completion so they remain available for reuse.
2026-03-29 18:40:29 +08:00
Naiyuan Qing
ab4058b1e4 Merge pull request #170 from multica-ai/NevilleQingNY/ui-polish
fix(ui): restore issue identifier in breadcrumb
2026-03-29 17:59:13 +08:00
Naiyuan Qing
04cba6fc46 fix(ui): restore issue identifier in breadcrumb
Re-add issue.identifier (e.g. MUL-42) to the breadcrumb nav that was
incorrectly removed during merge conflict resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:57:12 +08:00
Jiayuan Zhang
5211947104 Merge pull request #168 from multica-ai/forrestchang/agent-arch-review
feat: session persistence, agent instructions, and on_comment trigger
2026-03-29 17:54:22 +08:00
Naiyuan Qing
6b84186fb2 Merge pull request #169 from multica-ai/NevilleQingNY/invite-inbox-realtime
feat: inbox workspace isolation, realtime routing, and UI polish
2026-03-29 17:52:44 +08:00
Jiayuan
b112d1f1ae feat(tasks): add coalescing queue and task lifecycle guards
- Coalescing queue: use HasPendingTaskForIssue (queued/dispatched only)
  instead of HasActiveTaskForIssue so comments during a running task
  enqueue exactly one follow-up task that picks up all new comments.
- Stale task cleanup: runtime sweeper now fails orphaned tasks when
  their runtime goes offline (daemon crash/network partition).
- Cancel-aware daemon: handleTask checks task status after execution
  and discards results if the task was cancelled mid-run (e.g. reassign).
- Terminal issue guard: ClaimTaskForRuntime auto-cancels pending tasks
  for done/cancelled issues instead of executing them.
- Race condition safety net: unique partial index ensures at most one
  pending task per issue at the DB level.
2026-03-29 17:52:35 +08:00
Naiyuan Qing
b0a6489ac6 fix(ui): polish issue detail — wider content, thinner comment border, smaller props text
Increase content max-width to 4xl, use ring-only border on comment
input, and reduce property sidebar value text to text-xs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:51:02 +08:00
Naiyuan Qing
3a8aec7d08 merge: resolve conflict in issue-detail breadcrumb
Keep identifier removed from breadcrumbs per design decision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:45:53 +08:00
Naiyuan Qing
abe3c5967a fix(ui): switch issue title to inline editable input
Replace the click-to-edit h1/Input toggle with a persistent inline
input that saves on blur/enter. Removes duplicate issue ID from
breadcrumbs. Uses a ref to prevent realtime updates from clobbering
in-progress edits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:43:12 +08:00
Naiyuan Qing
b6119878ec fix(ui): improve board card hover with shadow and text contrast
Cards now show muted title text by default that becomes full contrast on
hover, paired with a shadow-md elevation effect. Removes the old
opacity-based hover that made cards fade out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:43:06 +08:00
Naiyuan Qing
afbbf6ff25 feat(settings): redesign members tab with dropdown menu
Replace inline select + delete button with a three-dot dropdown menu
per member. Adds role descriptions, owner self-demotion protection, and
a cleaner list layout with ring border.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:42:59 +08:00
Naiyuan Qing
8d34e079e8 feat(invite): show toast when invited to workspace
member:added event now includes workspace_name. Frontend shows a toast
notification when the current user is invited. Also clears stale member
list on workspace switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:42:55 +08:00
Naiyuan Qing
4914f1d5dd feat(realtime): route personal events to target user only
Inbox events (new, read, archived, batch) are now sent via SendToUser
instead of broadcasting to the entire workspace room. Adds a new
Hub.SendToUser method. Also guards task broadcasts against deleted
issues to prevent global event leaks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:42:50 +08:00
Naiyuan Qing
4126073229 feat(inbox): scope all inbox queries by workspace_id
Inbox items were previously queried only by recipient, which leaked data
across workspaces. All list/count/batch operations now filter by
workspace_id from the X-Workspace-ID header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:42:45 +08:00
Jiayuan Zhang
4ceb7c27b0 Merge pull request #167 from multica-ai/forrestchang/runtime-review
refactor(runtimes): split page, add store, time range
2026-03-29 17:06:54 +08:00
Jiayuan
87e76078ce revert(runtimes): remove runtime delete functionality
Remove the full-stack delete feature (SQL query, Go handler, route,
API client method, and UI delete button with confirmation dialog).
2026-03-29 17:05:36 +08:00
Jiayuan
ceba8556f5 refactor(runtimes): split monolithic page, add zustand store, time range selector, and delete support
- Split 1189-line runtimes-page.tsx into focused sub-components (list, detail, ping, usage, 5 chart files, shared UI, utils)
- Add useRuntimeStore zustand store for shared runtime state across pages (agents page now uses it too)
- Add 7d/30d/90d time range selector to usage charts
- Add full-stack runtime delete: SQL query, Go handler, API route, frontend with confirmation dialog
- Remove unused daemon:heartbeat WS listener (server never broadcasts it)
2026-03-29 17:02:25 +08:00
Jiayuan
32f795e1ef feat(agent): trigger agent on member comment (on_comment trigger)
When a member comments on an issue assigned to an agent, automatically
enqueue a new task if the agent has on_comment trigger enabled (or no
triggers configured). Combined with session persistence, the agent
resumes its prior conversation context and sees the new feedback.

- Add HasActiveTaskForIssue query to prevent duplicate task enqueue
- Refactor shouldEnqueueAgentTask into reusable isAgentTriggerEnabled
- Add shouldEnqueueOnComment with active-task and status guards
- Call trigger logic from CreateComment handler
2026-03-29 17:01:19 +08:00
Jiayuan
5b2c61cfab feat(agent): add instructions field for agent persona/identity
Add an `instructions` text field to the agent model, allowing users to
define each agent's role, expertise, and working style. Instructions are
injected into CLAUDE.md as an "Agent Identity" section so the agent
knows who it is on every task execution.

- Migration 021: add instructions column to agent table
- Backend: create/update/get agent handlers support instructions
- ClaimTask response includes instructions for daemon injection
- execenv: inject instructions into CLAUDE.md meta-skill
- Frontend: add Instructions tab to agent detail panel
2026-03-29 17:01:07 +08:00
Jiayuan
ffda18c809 feat(agent): add per-task session persistence for Claude Code resumption
Store the Claude Code session ID and working directory when a task
completes. On the next task for the same (agent, issue) pair, look up
the prior session and pass --resume <session_id> to Claude Code so
the agent retains conversation context across multiple tasks on the
same issue.

Changes:
- Migration 020: add session_id and work_dir columns to agent_task_queue
- CompleteAgentTask stores session_id and work_dir on completion
- GetLastTaskSession query retrieves prior session for (agent, issue)
- ClaimTaskByRuntime handler populates prior_session_id in response
- Daemon passes ResumeSessionID through to Claude backend Execute()
- Claude backend adds --resume flag when ResumeSessionID is set
2026-03-29 16:53:28 +08:00
Jiayuan Zhang
e819898185 Merge pull request #166 from multica-ai/forrestchang/readable-issue-ids
feat(issues): add human-readable issue identifiers (e.g. JIA-1)
2026-03-29 16:51:44 +08:00
Jiayuan
9fbac49f24 feat(issues): add human-readable issue identifiers (e.g. JIA-1)
Add per-workspace auto-incrementing issue numbers with a configurable
prefix, producing identifiers like "JIA-1" instead of truncated UUIDs.

Database:
- Add issue_prefix and issue_counter to workspace table
- Add number column to issue table with UNIQUE(workspace_id, number)
- Backfill existing issues with sequential numbers

Backend:
- Issue creation atomically increments counter in a transaction
- API responses include number and identifier fields
- Support issue lookup by identifier format (KEY-N)
- Workspace prefix auto-generated from name, customizable via API

Frontend:
- Display identifier in list rows and issue detail breadcrumb
- Add issue_prefix to Workspace type, number/identifier to Issue type
2026-03-29 16:49:55 +08:00
Jiayuan Zhang
42f72371bd Merge pull request #165 from multica-ai/forrestchang/feature-audit
feat(runtimes): add usage charts, activity heatmap, and hourly distribution
2026-03-29 15:46:39 +08:00
Jiayuan
36798b1d76 feat(runtimes): add usage charts, activity heatmap, and hourly distribution
Add comprehensive data visualization to the runtime detail page:
- Daily token usage stacked area chart and daily cost bar chart
- Model distribution donut chart with cost breakdown
- GitHub-style activity heatmap (13 weeks of daily token usage)
- Hourly task distribution bar chart with new backend endpoint
- Responsive 2-column grid layout for charts on wide screens

Backend: new GET /api/runtimes/{runtimeId}/activity endpoint
returning hourly task counts from agent_task_queue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 15:44:10 +08:00
Jiayuan Zhang
c5fed182e3 Merge pull request #163 from multica-ai/forrestchang/test-agent-review
Remove auto-init issue on agent creation
2026-03-29 14:24:14 +08:00
Jiayuan Zhang
6871163b0b Merge pull request #164 from multica-ai/forrestchang/fix-runtime-status
fix(runtime): add server-side sweeper to detect stale runtimes
2026-03-29 14:24:05 +08:00
Jiayuan
b3bbf92a1d fix(runtime): add server-side sweeper to detect stale runtimes
The only path to marking a runtime offline was the daemon's deregister
call on graceful shutdown. If the daemon crashed, was killed, or lost
network, the status stayed "online" forever. Add a background goroutine
that sweeps every 30s and marks runtimes offline after 45s without a
heartbeat (3 missed intervals).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 14:22:12 +08:00
Jiayuan
01ac0a81a9 refactor(agent): remove auto-init issue on agent creation
The createAgentInitIssue flow created a boilerplate "Initialize environment"
issue every time an agent was created, polluting the issue list with low-value
tasks. Agents naturally learn the project context when executing real work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 14:21:49 +08:00
Naiyuan Qing
586c3bf470 Merge pull request #162 from multica-ai/naiyuan/realtime-sync-refactor
feat(realtime): WS invalidation + refetch pattern
2026-03-29 13:51:22 +08:00
Naiyuan Qing
9236674667 feat(realtime): WS invalidation + refetch pattern, inbox bugfixes, UI polish
Refactor real-time sync from per-event precise mutations to WS-as-invalidation-signal + debounced refetch.

Backend:
- Add SubscribeAll to Event Bus — auto-broadcasts ALL events, eliminates manual 25-item allEvents list
- Add skill event constants to protocol, fix skill handler string literals
- Add title_changed activity tracking

Frontend:
- WSClient: add onAny() method for wildcard event subscription
- useRealtimeSync: rewrite to refreshMap + prefix routing + 100ms debounce
- Precise handlers only for side effects: workspace:deleted, member:removed, member:added (self-check)
- Reconnect now refetches all stores (fixes missing members/skills/workspace refresh)
- Stale-while-revalidate: fetch() only shows loading spinner on initial load, not on refetch
- Remove redundant useWSEvent in agents/page.tsx and skills-page.tsx
- WSClient.disconnect() now clears all handler registrations

Inbox bugfixes:
- Unify sidebar badge count with page count via dedupedItems + unreadCount in store
- Sort by time DESC (removed severity-first ordering)
- Ellipsis on truncated detail labels

UI:
- Status/Priority pickers: replace RadioGroup with MenuItem for auto-close on selection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:49:40 +08:00
Jiayuan Zhang
26318f6afd Merge pull request #161 from multica-ai/forrestchang/explore-cli-cmds
feat(cli): restructure CLI commands for better UX
2026-03-29 01:49:17 +08:00
Jiayuan
38d595d81d feat(cli): restructure CLI commands for better UX
- Add top-level `multica login` that combines auth + workspace auto-discovery
- Restructure daemon into subcommands: start, stop, status, logs
- Add background daemon mode with PID management
- Add daemon deregistration on shutdown (new API endpoint + SQL query)
- Remove unused commands: runtime list, status, agent get/delete/stop
- Make `config` show config directly instead of requiring `config show`
- Update README to reflect new CLI structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 01:43:45 +08:00
Naiyuan Qing
19810363eb Merge pull request #160 from multica-ai/naiyuan/activity-log
feat: unified activity timeline, comment replies, and inbox notifications
2026-03-29 00:24:11 +08:00
Naiyuan Qing
e72f5f0801 feat(inbox): add priority/due_date notifications, structured details, and hover card
- Add missing notifications for priority_changed and due_date_changed events
- Publish priority_changed and due_date_changed flags from UpdateIssue handler
- Add details JSONB column to inbox_item (migration 019) for structured change data
- Store from/to values in details for status, priority, assignee, and due_date changes
- Notification titles now use plain issue title; details carry structured context
- Add human-readable label maps (statusLabels, priorityLabels) in notification listeners
- Update inbox handler responses to include details field
- Frontend: InboxDetailLabel renders rich subtitles per notification type
  - Status: "Set status to ● In Progress" with StatusIcon
  - Priority: "Set priority to ◆ High" with PriorityIcon
  - Assigned: "Assigned to Bob" with resolved actor name
  - Due date: "Set due date to Apr 20"
  - Comment: truncated comment body preview
- Frontend: HoverCard on inbox items shows issue title + description context
- Add due_date_changed to InboxItemType and typeLabels
- Add tests for priority_changed and due_date_changed notifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:22:17 +08:00
Naiyuan Qing
b2ee151306 fix(activity): address code review feedback and improve timeline UX
- Extract shared timeAgo utility, remove duplicates from comment-card and issue-detail
- Remove unused replies prop from CommentCard
- Fix recursive delete to remove all descendant replies, not just direct children
- Improve formatActivity with human-readable status/priority labels and actor names
- Validate parent comment exists and belongs to same issue before creating reply
- Add priority_changed activity recording in activity listeners
- Fix activity SQL query to sort ASC (was DESC, then re-sorted in handler)
- Fix reply-input layout alignment and test submit button selector
- Minor: .gitignore additions, button dark mode aria-expanded fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:21:46 +08:00
Naiyuan Qing
5d2e62ccde fix(comments): fix padding, separate CommentInput from ReplyInput
- Remove Card double padding (Card py-4 + inner px-4 was too much, override with !py-0)
- Use lighter border-border/50 for reply separators inside Card
- Create CommentInput component for bottom "Leave a comment" — no avatar, full
  width editor, submit button in footer row below editor
- ReplyInput stays for in-card "Leave a reply" — has avatar, compact inline layout
- Two different components for two different use cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:13:03 +08:00
Naiyuan Qing
0c8738676c refactor(comments): flat thread layout in one Card (Linear-style)
Replace recursive Card-in-Card with flat thread layout:
- One Card per comment thread, parent + all replies flat inside
- Replies separated by border-t, not nested Cards
- CommentRow component handles each individual comment (header + content + edit)
- Three-dot menu shows active state when open (data-[popup-open])
- ReplyInput simplified: avatar + editor + submit button, no extra border container
- Nested replies collected recursively but rendered flat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:39:02 +08:00
Naiyuan Qing
56c06ec13b refactor(timeline): use avatar for activity entries, remove filter, allow nested replies
- Activity entries now show ActorAvatar instead of a small dot (consistent with comments)
- Remove All/Comments/Activity filter toggle (comments are just a type of activity)
- Remove one-level reply restriction in backend (allow nested threading)
- Remove unused Circle import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:13:23 +08:00
Naiyuan Qing
6cc8f7586a fix(comments): cascade delete replies in DB when parent comment is deleted
Change parent_id FK from ON DELETE SET NULL to ON DELETE CASCADE.
Deleting a parent comment now deletes all its replies at the DB level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:04:35 +08:00
Naiyuan Qing
8a9ae0102d fix(comments): cascade delete replies when parent comment is deleted
When a parent comment is deleted, remove all its replies from the timeline
instead of promoting them to top-level (orphaned replies have no context).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:03:46 +08:00
Naiyuan Qing
ba3c8e1b3f fix(activity): address code review feedback
- Fix activity:created WS payload to match frontend expectations
  (issue_id at top level, entry as TimelineEntry object)
- Promote child comments to top-level when parent is deleted
  (both in handleDeleteComment and WS comment:deleted handler)
- Enforce one-level reply nesting: reject replies to replies with 400

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:02:25 +08:00
Naiyuan Qing
e7fe6ea79b feat(activity): unified activity timeline with comment reply support
Replace the comment-only list with a Linear-style unified timeline that
interleaves field changes and comments chronologically.

Backend:
- activity_listeners.go: records field changes (status, assignee, description,
  task completed/failed) to activity_log table on domain events
- Timeline API: GET /api/issues/{id}/timeline merges activity_log + comments
  sorted by created_at
- Comment reply: parent_id column + handler support for threading

Frontend:
- Unified timeline replaces comment list: activity entries as compact muted
  lines, comments as Card components with reply threading
- Filter toggle (All / Comments / Activity)
- Reply UI: inline editor under comments with Cancel/Reply buttons
- Real-time sync for activity:created + comment events
- 10 new Go tests, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:53:08 +08:00
Naiyuan Qing
3bb79564ed Merge pull request #159 from multica-ai/naiyuan/subscriber-notifications
feat(notifications): subscriber-driven notification system
2026-03-28 21:17:42 +08:00
Naiyuan Qing
c2e8071c64 fix(subscribers): remove all reason labels from subscriber popover
Clean list — just checkbox, avatar, and name. No Creator/Assignee labels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:14:58 +08:00
Naiyuan Qing
89c617b39e fix(subscribers): only show Creator and Assignee labels in subscriber popover
Remove commenter/mentioned/manual reason labels — users only need to see
who created and who is assigned. Other reasons add noise without value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:11:09 +08:00
Naiyuan Qing
2ec3e4eb89 fix(subscribers): use composite key (user_id + user_type) in subscriber:removed WS handler
Consistent with subscriber:added handler. Prevents removing a member subscriber
when an agent with the same UUID is unsubscribed (or vice versa).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:07:48 +08:00
Naiyuan Qing
f410799ab5 fix(subscribers): prevent duplicate subscriber entries from optimistic update + WS race
When subscribing, both the optimistic setSubscribers and the WS subscriber:added
event could add the same entry. Now both paths check for existing entries using
(user_id + user_type) composite key before appending.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:59:35 +08:00
Naiyuan Qing
8a96aa698c fix(subscribers): deduplicate members list in subscriber popover
Members array from workspace store can contain duplicate user_ids (e.g. user
added to workspace twice). Filter by unique user_id before rendering to prevent
React duplicate key warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:57:41 +08:00
Naiyuan Qing
b8fa71462a fix(subscribers): pass user_type to subscribe/unsubscribe API for correct agent handling
Root cause: backend hardcoded UserType="member" in subscribe/unsubscribe handlers,
so unsubscribing an agent actually tried to delete a member record (no-op), and
the WS event broadcast wrong user_type causing other subscribers' UI to break.

- Backend: accept optional user_type in subscribe/unsubscribe request body
- Frontend: pass userType through API client to backend
- Fixes: clicking agent checkbox no longer affects member subscriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:56:37 +08:00
Naiyuan Qing
df6f6584c3 fix(subscribers): fix duplicate key error, add agents to subscriber popover
- Fix duplicate React key by using composite key (type-id) for avatars and command items
- Add Agents section to subscriber Command popover (members + agents both selectable)
- Fix subscriber matching to use both user_type and user_id (prevents cross-type collisions)
- Add max-h-64 to CommandList to prevent overflow
- toggleSubscriber now accepts userType parameter for proper agent support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:51:50 +08:00
Naiyuan Qing
05c0471945 feat(subscribers): interactive subscriber management with AvatarGroup + Command popover
- Backend: subscribe/unsubscribe API now accepts optional user_id to manage other users
- Frontend: replace manual avatar stacking with shadcn AvatarGroup component
- Frontend: replace disabled-checkbox Popover with Command+Popover combo (Linear-style)
  - Search/filter workspace members
  - Click to toggle subscription (checkbox)
  - Shows reason badge (Assignee, Creator, etc.) for auto-subscribed users
- Clean up duplicate status_change type (keep only status_changed)
- All tests pass (Go + TypeScript)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:48:20 +08:00
Naiyuan Qing
ec188a36c9 refactor(issues): move subscribers UI from sidebar to Activity header (Linear-style)
Move subscriber controls from right sidebar to Activity section header:
- Left: "Activity" title, Right: "Subscribe/Unsubscribe" button + avatar stack
- Click avatar stack opens Popover showing all subscribers with checkboxes
- Non-subscribers shown dimmed in the popover list
- Stacked avatar display (overlapping, max 5 + count badge)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:36:50 +08:00
Naiyuan Qing
6a09714041 fix(inbox): add missing notification type labels for subscriber-driven notifications
Add new_comment, unassigned, assignee_changed, priority_changed, status_changed
to InboxItemType and typeLabels so inbox renders proper labels instead of raw
type strings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:29:26 +08:00
Naiyuan Qing
5b9241b74f fix(notifications): address code review feedback
- Add comment explaining subscriber→notification listener registration order in main.go
- Add issue_status field to notifySubscribers and notifyDirect (fixes missing StatusIcon in inbox)
- Backfill existing commenters as subscribers in migration 016
- Add TODO comment for @mention duplicate notification prevention (deferred until @mention feature is enabled)
- Add context.Background() usage note for future bus-level timeout improvements
- Add toast error feedback on subscribe/unsubscribe failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 20:24:07 +08:00
Naiyuan Qing
bfe9498def feat(notifications): replace hardcoded inbox notifications with subscriber-driven model
Replace inbox_listeners.go with a subscriber-driven notification system:

- Add issue_subscriber table with auto-subscribe on create/assign/comment
- New subscriber_listeners.go: maintains subscriber data on domain events
- New notification_listeners.go: notifySubscribers (fanout to all subscribers
  minus actor) and notifyDirect (targeted, punches through unsubscribe)
- Subscriber API: list/subscribe/unsubscribe endpoints
- Frontend: subscribers section in issue detail sidebar with real-time sync
- Frontend: inbox notification grouping by (issue_id, type, actor_id)
- Remove createInboxForIssueCreator from task.go (unified through event bus)
- 21 new Go tests, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:33:20 +08:00
Naiyuan Qing
5fc03c61fe Merge pull request #158 from multica-ai/forrestchang/theme-card-selector
feat(settings): visual theme preview card selector
2026-03-28 16:27:12 +08:00
Naiyuan Qing
6b93e3ba9c fix(tests): update issue page tests for new header and view store shape
- Add missing view-store mock fields (sortBy, sortDirection, cardProperties,
  listCollapsedStatuses, toggleListCollapsed) and exported constants
- Add SortableContext mock to @dnd-kit/sortable
- Fix filter button assertions (Status/Priority: All → Filter/Display)
- Fix issue detail test: use getAllByText for duplicate title text
- Fix breadcrumb test: workspace name shown instead of "Issues"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:25:20 +08:00
Naiyuan Qing
572d033b95 feat(ui): comprehensive UI consistency fixes and list view accordion redesign
- Runtime page: add ResizablePanelGroup with persistent layout, fix scroll
- Agents page: replace hand-rolled dropdowns with shadcn Popover/DropdownMenu,
  remove redundant wrapper div, fix header height to h-12
- Skills page: widen create dialog to sm:max-w-md, stabilize tab height
- Settings: use variant="destructive" on AlertDialogAction instead of hardcoded className
- Issues list view: rewrite with base-ui Accordion grouped by status,
  show all statuses (including empty), add per-group create button,
  persist expand/collapse state, apply sort settings
- Issues header: show filtered issue count next to New Issue button
- Extract shared sortIssues utility from board-column for reuse
- Remove redundant StatusIcon from ListRow (already grouped by status)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:18:42 +08:00
Naiyuan Qing
1d08057dd8 feat(settings): replace theme buttons with visual preview card selector
Replace simple text buttons with macOS-style window preview cards for
Light/Dark/System theme selection, using pure CSS miniature window mockups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:03:47 +08:00
Jiayuan Zhang
fd9d2e2290 Merge pull request #157 from multica-ai/forrestchang/kanban-redesign
feat(issues): redesign kanban board with drag sorting and display settings
2026-03-28 01:14:39 +08:00
Jiayuan
f050ab1ae2 feat(issues): add inline property editing on kanban cards and calendar date picker
- Add inline pickers on board cards for priority, assignee, and due date
- Add custom trigger prop to PriorityPicker, AssigneePicker, DueDatePicker for styling control
- Replace due date quick options with Calendar-based DueDatePicker in issue detail sidebar
- Use PickerWrapper to stop event propagation from pickers to Link/drag handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 01:13:18 +08:00
Jiayuan Zhang
14ae1b9aff Merge pull request #156 from multica-ai/forrestchang/agent-issue-flow
feat(daemon): inject skills into agent-native directories
2026-03-28 00:52:32 +08:00
Jiayuan
7cd9110628 feat(issues): redesign kanban board with drag sorting, filters, and display settings
- Redesign card layout: priority label, description line, subtle shadow, configurable properties
- Wider columns (280px) with rounded background, count badge, and card spacing
- Add SortableContext for within-column drag reordering with fractional position indexing
- Fix collision detection to prefer card targets over column droppables for reliable down-drag
- Add hidden columns panel on right side with show/hide toggle
- Consolidate header into Filter popover (status + priority) and Display popover (ordering + card properties)
- Auto-switch to manual sort when cards are dragged to preserve drag ordering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 00:50:29 +08:00
Jiayuan
46144646c5 feat(daemon): inject skills into agent-native directories
Write skills to provider-native paths so agents discover them
automatically instead of relying on manual path references in
CLAUDE.md/AGENTS.md.

- Claude: write to {workDir}/.claude/skills/ (native discovery)
- Codex: write to per-task CODEX_HOME/skills/ with auth/config
  seeded from ~/.codex/ (symlink auth.json, copy config files)
- Fallback: keep .agent_context/skills/ for unknown providers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 00:47:00 +08:00
Jiayuan Zhang
1b505c3a21 Merge pull request #155 from multica-ai/forrestchang/codex-token-parse
fix(daemon): fix Codex token usage parsing from session logs
2026-03-28 00:04:40 +08:00
Jiayuan
241acb3eac fix(daemon): fix Codex token usage parsing from session logs
The parser read `payload.msg` but Codex JSONL files store token data at
`payload.info`. Also adds model tracking from `turn_context` events,
`last_token_usage` fallback, and `cache_read_input_tokens` field support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 00:03:47 +08:00
Jiayuan Zhang
e860691124 Merge pull request #154 from multica-ai/forrestchang/skills-tree-view
feat(skills): file-browser tree view with markdown preview
2026-03-27 23:57:28 +08:00
Jiayuan
8de700620d feat(skills): replace skill detail with file-browser tree view
Redesign the skill detail panel to use a directory tree + file viewer
layout, similar to a file browser. SKILL.md and supporting files are
shown in a collapsible tree on the left; selecting a file renders its
content on the right with markdown preview (including YAML frontmatter
parsing) and an edit toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 23:54:32 +08:00
Jiayuan Zhang
88294fada6 Merge pull request #153 from multica-ai/forrestchang/skill-add-ux-review
feat(skills): improve import skill UX with source cards
2026-03-27 23:44:26 +08:00
Jiayuan
f4ff01210b feat(skills): improve import skill UX with source cards
Replace the subtle inline badge with two always-visible source cards
that show URL formats for ClawHub and Skills.sh, highlighting the
detected source as the user types. Import button now shows
source-specific loading text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 23:42:08 +08:00
Jiayuan Zhang
6395a74661 Merge pull request #152 from multica-ai/forrestchang/paperclip-audit
Improve local CLI auth and skills UX
2026-03-27 18:43:52 +08:00
Jiayuan
ed426872cc Improve local CLI auth and skills UX 2026-03-27 18:32:56 +08:00
LinYushen
6d2a0b45d2 refactor: decouple task lifecycle from issue status (#151)
* refactor: decouple task lifecycle from issue status, add daemon health server

- Remove automatic issue status changes from StartTask (in_progress),
  CompleteTask (in_review), and FailTask (blocked) in task service.
  Issue status is now fully managed by the agent via `multica issue status`.
- Update agent prompt and meta skill to instruct agents to manage issue
  status themselves (in_progress → done/in_review/blocked).
- Add daemon health HTTP server on 127.0.0.1:19514 with /health endpoint
  exposing pid, uptime, agents, and workspaces. Fail fast if port is taken
  (another daemon already running).
- Update `multica status` to check both server and daemon health.
- Add Save button to repos section in workspace settings UI.

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

* refactor(daemon): simplify prompt, fix runtime config path, improve task error logging

- Slim down BuildPrompt to a minimal hint; detailed workflow now lives in CLAUDE.md/AGENTS.md
- Write CLAUDE.md to workDir root instead of .claude/CLAUDE.md
- Fix git-exclude pattern (.claude → CLAUDE.md)
- Decouple task queue reconciliation from issue status changes (agents manage status via CLI)
- Add diagnostic logging when CompleteTask/FailTask fail due to unexpected task state

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

* fix(task): use task_completed/task_failed inbox notification types

FailTask was sending "agent_blocked" which conflates agent crash with
issue-level blocked status. Align notification types with the new
decoupled model: task_completed and task_failed. Update frontend types
and labels accordingly.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:30:21 +08:00
LinYushen
8bd476f47c Merge pull request #150 from multica-ai/fix/pr-149-review-fixes
fix: address PR #149 review findings
2026-03-27 16:19:47 +08:00
yushen
4d8b2edb2d fix: address PR #149 review findings
- Replace custom contains/searchString with strings.Contains in tests
- Fix variable shadow (r -> reposJSON) in workspace handler
- Wire daemon auth token + server URL into spawned agent env vars
- Remove unused CreateAgentTaskWithContext query (dead code after refactor)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:15:58 +08:00
LinYushen
327973be08 Merge pull request #149 from multica-ai/feat/cli-issue-commands
feat(cli): add issue management commands
2026-03-27 16:12:03 +08:00
yushen
bae2926370 feat(web): add repos management UI to workspace settings
Add a Repositories section to workspace settings where admins can
add/remove GitHub repo URLs with descriptions. Agents use these repos
to clone and work on code.

- Add WorkspaceRepo type and export from shared/types
- Update API client updateWorkspace to accept repos
- Add repos section in workspace-tab.tsx with add/remove UI
- Fix test helpers to include repos field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:05:54 +08:00
yushen
83111761db feat(workspace): add repos JSONB field for GitHub repository URLs
Add a `repos` JSONB column to the workspace table for storing
associated repository URLs and descriptions. This enables the daemon
to clone repos and set up git worktrees for agent task execution.

Structure: [{"url": "https://github.com/org/repo", "description": "..."}]

- Migration 014: adds repos column with default '[]'
- UpdateWorkspace query: supports updating repos
- Workspace API: returns repos in GET, accepts in PUT

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:49:30 +08:00
yushen
1deae2a1e9 refactor(daemon): remove context snapshot, let agent fetch data via CLI
Replace the frozen context snapshot pattern with a CLI-driven approach:
agents now use `multica` CLI commands to fetch issue details, comments,
and workspace context on demand, always getting the latest data.

- Remove buildContextSnapshot and snapshot generation from enqueue
- Claim endpoint now returns fresh agent name + skills from DB
- Daemon resolves provider from local runtimeIndex, not snapshot
- Prompt instructs agent to use `multica issue get` / `comment list`
- Meta skill (CLAUDE.md/AGENTS.md) documents all available CLI commands
- Skills still injected as filesystem files (static agent config)
- Simplify daemon types: remove TaskContext/IssueContext/RuntimeContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:31:22 +08:00
yushen
6733262a63 test(cli): add unit tests for issue command helpers
Tests for truncateID, formatAssignee, resolveAssignee (6 cases),
and validIssueStatuses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:12:20 +08:00
yushen
673ba09baf fix(cli): rune-safe truncateID, consistent client construction, add --output to status cmd
- Make truncateID use rune counting instead of byte length for unicode safety
- Refactor workspaceGet and workspaceMembers to use newAPIClient helper
  for consistent server-URL validation
- Add --output flag to issueStatusCmd for JSON output support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:12:16 +08:00
yushen
4a62b98c9a feat(cli): add workspace get and members commands
Expose workspace details (including context field) and member listing
via the CLI so agents can dynamically query workspace context instead
of relying on static snapshots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:54:40 +08:00
yushen
daecf7985c fix(cli): propagate API errors in resolveAssignee and fix rune-safe truncation
- resolveAssignee now reports actual API errors instead of silently
  falling through to "not found" when both member/agent fetches fail
- Comment content truncation uses rune count for correct CJK handling
- Remove unnecessary _ = aType discard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:38:11 +08:00
yushen
765ba8e380 feat(cli): add issue management commands
Add `multica issue` command group with subcommands for full issue
lifecycle management: list, get, create, update, assign, status,
and comment operations. Includes assignee resolution by name across
both workspace members and agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:33:53 +08:00
yushen
568b5f3a8f docs: update CLAUDE.md with logging, CI, and CLI details
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:33:38 +08:00
Jiayuan Zhang
da2f794a1b Merge pull request #148 from multica-ai/forrestchang/rm-agent-skills
chore: remove .agents/skills directory
2026-03-27 03:35:10 +08:00
Jiayuan
e07e294623 chore: remove .agents/skills directory
Remove all Claude Code skills (ui-ux-pro-max, shadcn, frontend-design,
web-design-guidelines) from the .agents directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 03:34:21 +08:00
Jiayuan Zhang
7e74c7b18c Merge pull request #147 from multica-ai/feat/add-claude-skills
feat: add Claude Code skills for UI/UX development
2026-03-27 00:42:07 +08:00
Jiayuan
acf9c625eb feat: add Claude Code skills for UI/UX development
Add four Claude Code skills to enhance frontend development workflow:
- frontend-design: production-grade frontend interfaces
- shadcn: shadcn/ui component management
- ui-ux-pro-max: comprehensive UI/UX design intelligence
- web-design-guidelines: Web Interface Guidelines compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:40:16 +08:00
Jiayuan Zhang
a6e9bb1f61 Merge pull request #146 from multica-ai/forrestchang/rm-knowledge-base
Remove knowledge base module and fix worktree port conflicts
2026-03-26 21:00:10 +08:00
Jiayuan
c1fc5c7bca chore(web): remove knowledge base module and fix worktree port conflicts
Remove the frontend-only knowledge base page, sidebar nav entry, and
unused BookOpen import. Fix setup-worktree to always generate unique
ports via init-worktree-env.sh instead of skipping when .env.worktree
already exists with default ports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 20:57:14 +08:00
Naiyuan Qing
88de467094 Merge pull request #145 from multica-ai/NevilleQingNY/rm-monorepo
Remove monorepo tooling, settings redesign, and UI improvements
2026-03-26 19:32:13 +08:00
Naiyuan Qing
488b41fc5e fix(test): update issues page test for empty state behavior change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:30:59 +08:00
Naiyuan Qing
5696b78a26 fix(web): show empty board/list view when no issues match filters
Previously the entire view was replaced with "No matching issues" text.
Now the board columns or list view render even when empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:29:00 +08:00
Naiyuan Qing
46708e8948 fix(web): resolve duplicate tiptap Link extension warning
Disable Link in StarterKit since it's configured separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:27:30 +08:00
Naiyuan Qing
395814b16a fix(test): update daemon tests after removing acceptance_criteria/context_refs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:26:35 +08:00
Naiyuan Qing
a500001093 refactor: remove acceptance_criteria and context_refs from issues
These fields were unused in practice. Removed from frontend types,
issue detail UI, backend handlers, daemon prompt/context, protocol
messages, SQL queries, and tests. DB columns retained with defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:24:34 +08:00
Naiyuan Qing
4052017c7a feat: settings redesign, rich text mentions, inbox listeners, and UI polish
- Refactor settings page into tabbed components (general, workspace, members, tokens, account)
- Move settings link from dropdown to sidebar nav
- Add @mention suggestions in rich text editor
- Expand inbox listeners with enhanced event handling
- Improve board column, issue detail, and create issue modal UX
- Update markdown rendering and code block styling
- Polish skills page layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:17:14 +08:00
Jiayuan Zhang
aeccd0ab51 Merge pull request #144 from multica-ai/forrestchang/local-runtime-tab
feat(runtimes): add Runtimes tab with usage tracking and connection test
2026-03-26 18:45:40 +08:00
Jiayuan
6ee034c6e9 merge: resolve conflicts after merging main
Adapt runtime features (usage tracking, ping, heartbeat) to main's
multi-workspace architecture. Update frontend imports from @multica/types
to @/shared/types after the package consolidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 18:37:56 +08:00
yushen
87b482e73c refactor(cli): move watch/unwatch under workspace subcommand
Reduces top-level command clutter by nesting watch/unwatch as workspace
subcommands. Removes redundant `watches` command since `workspace list`
already shows watched status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:28:44 +08:00
Jiayuan
903fbee55d feat(runtimes): add Runtimes tab with usage tracking and connection test
Add a new "Runtimes" sidebar tab to manage local agent runtimes with three
main capabilities: runtime status overview, token usage tracking (reading
Claude Code and Codex CLI local JSONL logs via daemon), and an interactive
connection test that sends a ping through the daemon to verify end-to-end
agent CLI connectivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 18:28:36 +08:00
Jiayuan Zhang
8a1067e2aa Merge pull request #143 from multica-ai/forrestchang/import-skill
feat(skills): add skill import from ClawHub and Skills.sh
2026-03-26 18:27:33 +08:00
Jiayuan
eeb4fee1b6 Merge remote-tracking branch 'origin/main' into forrestchang/import-skill 2026-03-26 18:23:59 +08:00
Jiayuan
9bc0366662 feat(skills): add skill import from ClawHub and Skills.sh
Support importing skills from external sources (clawhub.ai and skills.sh)
via a new POST /api/skills/import endpoint. The backend auto-detects the
source from the URL, fetches skill metadata and files, and creates the
skill in the workspace. The frontend CreateSkillDialog now has two tabs:
Create (manual) and Import (paste URL with source auto-detection badge).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 18:21:28 +08:00
Naiyuan Qing
82bd10c424 Merge pull request #142 from multica-ai/NevilleQingNY/rm-monorepo
refactor: remove unused monorepo tooling
2026-03-26 18:06:47 +08:00
Naiyuan Qing
a3d20d3644 fix(test): update login tests to match email verification flow
The login page was changed to email+OTP but tests still expected
the old name+email+password form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:05:23 +08:00
Naiyuan Qing
26d3ecd06d fix(ci): replace turbo commands with direct pnpm calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:01:24 +08:00
Naiyuan Qing
a882df14b4 chore: update pnpm-lock.yaml after removing turbo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:59:32 +08:00
Naiyuan Qing
d1f73bf7fc refactor: remove unused monorepo tooling
Delete packages/ui and packages/utils (zero imports from apps/web),
remove Turborepo, inline tsconfig.base.json into web's tsconfig,
and simplify root scripts to use pnpm --filter directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:57:11 +08:00
Naiyuan Qing
8684156ffc Merge pull request #140 from multica-ai/NevilleQingNY/fix-resizable-panels
feat: resizable sidebar, issue detail rewrite, package consolidation
2026-03-26 17:43:40 +08:00
Naiyuan Qing
939415db92 fix: resolve merge conflict in login page, adopt main's CLI auth flow
Take main's improved login page with extracted validateCliCallback
helper and cli_confirm step for existing sessions. Update import
from @multica/types to @/shared/types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:42:55 +08:00
Naiyuan Qing
8366e91b95 fix(auth): restore email verification login flow from main
The frontend ApiClient had a non-existent `/auth/login` endpoint.
Restored the two-step `sendCode` + `verifyCode` flow matching the
backend, including OTP input UI and CLI browser login callback support.
Also restored `IF NOT EXISTS` in migration 012 to prevent failures on
databases where the column already exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:40:54 +08:00
LinYushen
e9ae04012d Merge pull request #139 from multica-ai/refactor/remove-repos-root
refactor(daemon): multi-workspace support, remove pairing, CLI auth
2026-03-26 17:40:32 +08:00
yushen
e263064e87 fix(daemon): remove dead code and improve UX feedback
Remove unused ListWorkspaces/Workspace from daemon client, add log when
default workspace is set implicitly, document token reload limitation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:32:37 +08:00
yushen
e4a905c841 fix(daemon): improve error handling in auth and workspace loading
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:22:12 +08:00
yushen
852d18ca19 fix(web): add null checks for CLI auth callback and token
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:22:08 +08:00
Jiayuan Zhang
6fd0e2b319 Merge pull request #141 from multica-ai/forrestchang/fix-012-migration
fix(db): make inbox_actor migration idempotent
2026-03-26 17:21:46 +08:00
Naiyuan Qing
f70b34a50f fix: resolve merge conflicts with main, preserve PAT functionality
- Resolve conflicts in CLAUDE.md, client.ts, settings/page.tsx
- Migrate PAT types and API methods to @/shared/types + @/shared/api architecture
- Restore simplified login flow (login page, auth store, tests)
- Fix issue detail comment submit test (use fireEvent + useRef for mock)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:19:24 +08:00
Jiayuan
fa6b5b5ba3 fix(db): make inbox_actor migration idempotent
The renumbered 012_inbox_actor migration fails on databases where the
old 009_inbox_actor was already applied, since actor_type and actor_id
columns already exist. Use IF NOT EXISTS to handle both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 17:17:23 +08:00
yushen
12dec9f122 fix(daemon): add reload guard and atomic config writes
- Prevent concurrent reloadWorkspaces with a dedicated mutex
- SaveCLIConfig now writes to a temp file then renames for atomicity,
  preventing partial reads by the daemon's config watcher

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:11:15 +08:00
yushen
1e2ffddb72 docs: restore ~/.local/bin path option in README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:05:29 +08:00
yushen
3cf42e1913 docs: add brew install instructions to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:04:11 +08:00
yushen
ebda7eb9a8 docs: update README with CLI usage and remove outdated pairing docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:02:54 +08:00
yushen
844522ab1c refactor(cli): split workspace list and daemon watch commands
- `multica workspace list` — list all user workspaces from API (with
  watching indicator)
- `multica watch <id>` — add workspace to daemon watch list (top-level)
- `multica unwatch <id>` — remove from watch list (top-level)
- `multica watches` — show current daemon watch list (top-level)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:56:03 +08:00
yushen
4210fe69f4 feat(daemon): multi-workspace support with hot-reload
- Add `multica workspace watch/unwatch/list` CLI commands
- Daemon watches multiple workspaces from config's `watched_workspaces`
- Registers runtimes per workspace, polls all runtime IDs in round-robin
- Hot-reload: daemon detects config file changes every 5s and
  adds/removes workspaces without restart
- Remove `--workspace-id` flag from daemon (workspace selection is now
  purely config-driven via `multica workspace watch`)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:52:22 +08:00
Naiyuan Qing
2cf088ddf6 feat: resizable sidebar, issue detail rewrite, package consolidation
- Add drag-to-resize sidebar with localStorage persistence
- Rewrite issue detail page with Tiptap rich text editor, due date picker, acceptance criteria
- Redesign create-issue modal with pill-based property toolbar and expand/collapse
- Consolidate @multica/sdk and @multica/types into apps/web/shared/
- Simplify auth: remove verification codes, PATs, email service (dev-only login)
- Add 401 unauthorized handler to redirect expired sessions to login
- Fix due date format to send full RFC3339 timestamps
- Increase description editor debounce to 1500ms
- Remove arbitrary Tailwind values in create-issue modal
- Renumber migrations (inbox_actor 012→009), remove unused migrations
- UI polish across agents, settings, inbox, knowledge-base pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:47:04 +08:00
yushen
b9d2b88852 refactor(daemon): remove pairing flow, use CLI auth token instead
The daemon now reads the auth token from ~/.multica/config.json (set by
`multica auth login`) instead of requiring a browser-based pairing flow.
If not authenticated, it logs a message and exits.

Workspace ID is auto-resolved from the user's workspaces when not
explicitly set via flag/env.

Removed: daemon.json, pairing session flow, --config-path flag,
PairingSession type, PersistedConfig, LoadWorkspaceIDFromDaemonConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:39:57 +08:00
yushen
669b18e1c9 feat(auth): skip email verification for CLI login when already authenticated
When the browser has an existing valid session and the login page is
opened with cli_callback, show a one-click "Authorize CLI" confirmation
instead of requiring email verification again.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:33:49 +08:00
yushen
7b4a73c989 refactor(daemon): remove global ReposRoot, use per-task RepoPath from server
ReposRoot was a daemon-level config that locked all tasks to a single
git repo. Replace with RepoPath in TaskContext so the server can specify
the repo per task. When not provided, daemon falls back to directory mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:04:33 +08:00
LinYushen
aa3f927a37 Merge pull request #138 from multica-ai/feat/cli-browser-login
feat(auth): browser-based CLI login
2026-03-26 15:46:18 +08:00
yushen
072ee83ee8 fix(db): renumber inbox_actor migration to resolve 009 conflict
009_inbox_actor conflicted with 009_verification_code, causing
actor_type/actor_id columns to never be added and /api/inbox to 500.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:44:10 +08:00
yushen
de1b7e3377 fix(auth): reduce verification code rate limit from 60s to 10s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:44:05 +08:00
yushen
de322f7a51 fix(auth): improve CLI login callback page to match frontend design
Replace bare HTML with a styled card layout featuring dark/light mode
support, Multica brand icon, and auto-close behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:44:01 +08:00
yushen
fb2e286cfe test(cli): add unit tests for PostJSON API client method
Cover success response, error status codes, nil output, and workspace
header propagation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:04:50 +08:00
yushen
b9a4fa1a6d fix(auth): add CSRF state param, scheme validation, and .envrc to gitignore
Add state parameter to CLI browser login flow for CSRF protection — CLI
generates a random state, frontend passes it through, CLI verifies on
callback. Also restrict cli_callback to http: scheme only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:04:46 +08:00
yushen
788ba502f6 fix(auth): validate cli_callback to localhost and URL-encode callback param
- Prevent open redirect / JWT theft by only allowing localhost/127.0.0.1
  as cli_callback hostname
- URL-encode the callback URL in the login query string
- Simplify resolveAppURL to use os.Getenv directly (no phantom flag)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:55:03 +08:00
yushen
feb62ae0f8 feat(auth): add browser-based CLI login flow
`multica auth login` now opens the browser for email verification,
receives the JWT via localhost callback, and exchanges it for a PAT.
The legacy PAT-paste flow is preserved via `--token` flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:51:41 +08:00
LinYushen
5c9c2f69fd feat(auth): email verification login and personal access tokens
* feat(auth): add email verification login flow with 401 auto-redirect

Replace the old OAuth-based login with email verification codes:
- Backend: send-code / verify-code endpoints, verification_codes table (migration 009), rate limiting, Resend email service
- Frontend: two-step login UI (email → 6-digit OTP), auth store with sendCode/verifyCode
- SDK: ApiClient gains onUnauthorized callback; 401 responses auto-clear token and redirect to /login
- Fix login button staying disabled due to global isLoading state

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

* fix(auth): add brute-force protection, redirect loop guard, and expired code cleanup

- VerifyCode: increment attempts on wrong code, reject after 5 failed tries (migration 010)
- onUnauthorized: skip redirect if already on /login to prevent infinite loops
- SendCode: best-effort cleanup of expired verification codes older than 1 hour

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

* feat(auth): add master verification code for non-production environments

Allow code "888888" to bypass email verification in non-production
environments to simplify development and testing workflows.

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

* feat(auth): add personal access tokens for CLI and API authentication

Add full-stack PAT support: users create tokens in Settings, CLI authenticates
via `multica auth login`. Server stores SHA-256 hashes only. Auth middleware
extended to accept both JWTs and PATs (distinguished by `mul_` prefix).

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:32:30 +08:00
Naiyuan Qing
a997bcfec0 Merge pull request #136 from multica-ai/NevilleQingNY/logging-and-ui-polish
feat: logging, UI polish, and issues page rewrite
2026-03-26 12:40:56 +08:00
Naiyuan Qing
9ff19214de test(issues): update page tests for new component structure
Update mocks for useIssueViewStore, view-store, dnd-kit, and
workspace breadcrumb. Adjust assertions for DropdownMenu-based
filters and multi-select state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:39:55 +08:00
Naiyuan Qing
bc39abc6ed feat: inbox actor tracking, issue detail extraction, UI polish
- Add actor_type/actor_id to inbox items for proper attribution
- Extract issue detail into features/issues/components/issue-detail.tsx
- Inbox page and store updates for actor-based notifications
- Sidebar, layout, and actor-avatar refinements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:36:12 +08:00
Naiyuan Qing
586a4916d1 feat(issues): rewrite issues page with component decomposition and persisted filters
- Decompose monolithic 472-line page.tsx into focused components:
  board-card, board-column, board-view, list-row, list-view,
  issues-header, issues-page
- Add view-store with Zustand persist for viewMode and multi-select
  status/priority filters
- Fix kanban DnD with pointerWithin + closestCenter collision detection
- Add workspace breadcrumb header and Linear-style filter dropdowns
  using DropdownMenu with CheckboxItem for multi-select
- Status filter hides kanban columns, priority filter hides cards
- Drop target highlight with bg-accent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:36:01 +08:00
Naiyuan Qing
655aa40732 feat: issue detail extraction, inbox enhancements, misc UI polish
- Extract IssueDetail into reusable component
- Inbox: add body/type fields, bulk actions, read state
- Pages: consistent layout patterns
- Workspace avatar, markdown, realtime sync improvements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:57:58 +08:00
Naiyuan Qing
59816b807b feat(ui): Linear-style status icons with progress circle base
Refactor status icons to use a shared ProgressCircle SVG base with
pie-arc fill. Backlog, Todo, InProgress, InReview, Done, Cancelled,
Blocked all use the unified component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:57:42 +08:00
Naiyuan Qing
459d9745b9 feat(ui): route persistence, sidebar active fix, header spacing
- Persist last visited path via Zustand persist, restore on login/root
- Sidebar: exact match for active state (issue detail no longer highlights Issues)
- Sidebar header: increase vertical padding
- Inbox unread count: simplified to text-xs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:57:32 +08:00
Naiyuan Qing
8983a9fefa feat(logging): add structured logging across server and SDK
Replace raw fmt/log calls with structured slog logger (Go) and
console-based logger (TypeScript). Add request logging middleware.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:57:11 +08:00
Jiayuan Zhang
6da87dd725 Merge pull request #135 from multica-ai/forrestchang/rename-super-mutica
chore: rename super-multica references to multica
2026-03-25 21:09:11 +08:00
Jiayuan
1988c45fb0 chore: rename super-multica references to multica
The project was previously called "super-multica" but has been renamed
to "multica". Update all remaining references in docker-compose, goreleaser,
README, and local development docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 21:01:39 +08:00
844 changed files with 84014 additions and 16698 deletions

38
.dockerignore Normal file
View File

@@ -0,0 +1,38 @@
# Dependencies
node_modules
.pnpm-store
# Build outputs
.next
dist
server/bin
server/tmp
# Git
.git
.gitignore
# Environment
.env
.env.*
!.env.example
# IDE
.idea
.vscode
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test
e2e/test-results
coverage
# Docs
docs/
# Desktop app (not needed for web self-hosting)
apps/desktop

View File

@@ -21,13 +21,31 @@ MULTICA_CODEX_MODEL=
MULTICA_CODEX_WORKDIR=
MULTICA_CODEX_TIMEOUT=20m
# Email (Resend)
RESEND_API_KEY=
RESEND_FROM_EMAIL=noreply@multica.ai
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
NEXT_PUBLIC_GOOGLE_CLIENT_ID=
# S3 / CloudFront
S3_BUCKET=
S3_REGION=us-west-2
CLOUDFRONT_KEY_PAIR_ID=
CLOUDFRONT_PRIVATE_KEY_SECRET=multica/cloudfront-signing-key
CLOUDFRONT_PRIVATE_KEY=
CLOUDFRONT_DOMAIN=
COOKIE_DOMAIN=
# Frontend
FRONTEND_PORT=3000
FRONTEND_ORIGIN=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws
# Remote API (optional) — set to proxy local frontend to a remote backend
# Leave empty to use local backend (localhost:8080)
# REMOTE_API_URL=https://multica-api.copilothub.ai

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
# Ensure shell scripts always use LF line endings (needed for Docker on Windows)
*.sh text eol=lf
docker/entrypoint.sh text eol=lf
# Default behavior
* text=auto

34
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,34 @@
## What
<!-- What does this PR do? Keep it to 1-3 sentences. -->
## Why
<!-- Why is this change needed? Link the related issue. -->
Closes #<!-- issue number -->
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor / code improvement
- [ ] Documentation
- [ ] CI / infrastructure
- [ ] Other (describe below)
## How to Test
<!-- How can a reviewer verify this works? Steps, commands, or screenshots. -->
## Checklist
- [ ] `make check` passes (typecheck, unit tests, Go tests, E2E)
- [ ] Changes follow existing code patterns and conventions
- [ ] No unrelated changes included
## AI Disclosure (optional)
<!-- If AI tools were used: -->
<!-- - Which tool? (e.g., Claude Code, Copilot, Cursor) -->
<!-- - What prompt did you use? Sharing your prompt helps others learn and lets reviewers understand intent. -->

View File

@@ -29,17 +29,8 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Restore Turbo cache
uses: actions/cache@v5
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
turbo-${{ runner.os }}-
- name: Build, type check, and test
run: pnpm turbo build typecheck test
run: pnpm build && pnpm typecheck && pnpm test
backend:
runs-on: ubuntu-latest
@@ -66,7 +57,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.26.1"
cache-dependency-path: server/go.sum
- name: Build

10
.gitignore vendored
View File

@@ -2,11 +2,12 @@ node_modules
dist
*.log
.DS_Store
.envrc
# build outputs
.turbo
.next
out
.turbo
build
bin
dist-electron
@@ -35,6 +36,13 @@ apps/web/test-results/
# local settings
.claude/
.tool-versions
# feature tracking
_features/
# runtime
*.pid
# platform specific
*.dmg

View File

@@ -11,6 +11,7 @@ builds:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.ShortCommit}}
- -X main.date={{.Date}}
env:
- CGO_ENABLED=0
goos:
@@ -45,9 +46,9 @@ brews:
branch: main
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
directory: Formula
homepage: "https://github.com/multica-ai/super-multica"
homepage: "https://github.com/multica-ai/multica"
description: "Multica CLI — local agent runtime and management tool for the Multica platform"
license: "MIT"
license: "Apache-2.0"
install: |
bin.install "multica"
test: |

274
AGENTS.md Normal file
View File

@@ -0,0 +1,274 @@
# Repository Guidelines
This file provides guidance to AI agents when working with code in this repository.
## Project Context
Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens.
- Agents can be assigned issues, create issues, comment, and change status
- Supports local (daemon) and cloud agent runtimes
- Built for 2-10 person AI-native teams
## Architecture
**Go backend + standalone Next.js frontend.**
- `server/` — Go backend (Chi router, sqlc for DB, gorilla/websocket for real-time)
- `apps/web/` — Next.js 16 frontend (App Router) — self-contained, no shared package dependencies
- `e2e/` — Playwright end-to-end tests
- `scripts/` and root `Makefile` — local setup and verification
### Web App Structure (`apps/web/`)
The frontend uses a **feature-based architecture** with four layers:
```
apps/web/
├── app/ # Routing layer (thin shells — import from features/)
├── features/ # Business logic, organized by domain
├── shared/ # Cross-feature utilities (api client, types, logger)
├── test/ # Shared test utilities and setup
├── public/ # Static assets
```
**`app/`** — Next.js App Router pages. Route files should be thin: import and re-export from `features/`. Layout components and route-specific glue (redirects, auth guards) live here. Shared layout components (e.g. `app-sidebar`) stay in `app/(dashboard)/_components/`.
**`features/`** — Domain modules, each with its own components, hooks, stores, and config:
| Feature | Purpose | Exports |
|---|---|---|
| `features/auth/` | Authentication state | `useAuthStore`, `AuthInitializer` |
| `features/workspace/` | Workspace, members, agents | `useWorkspaceStore`, `useActorName` |
| `features/issues/` | Issue state, components, config | `useIssueStore`, icons, pickers, status/priority config |
| `features/inbox/` | Inbox notification state | `useInboxStore` |
| `features/realtime/` | WebSocket connection + sync | `WSProvider`, `useWSEvent`, `useRealtimeSync` |
| `features/modals/` | Modal registry and state | Modal store and components |
| `features/skills/` | Skill management | Skill components |
**`shared/`** — Code used across multiple features:
- `shared/api/``ApiClient` (REST) and `WSClient` (WebSocket) for backend communication, plus the `api` singleton.
- `shared/types/` — Domain types (Issue, Agent, Workspace, etc.) and WebSocket event types.
- `shared/logger.ts` — Logger utility.
### State Management
- **Zustand** for global client state — one store per feature domain (`features/auth/store.ts`, `features/workspace/store.ts`, `features/issues/store.ts`, `features/inbox/store.ts`).
- **React Context** only for connection lifecycle (`WSProvider` in `features/realtime/`).
- **Local `useState`** for component-scoped UI state (forms, modals, filters).
- Do not use React Context for data that can be a zustand store.
**Store conventions:**
- One store per feature domain. Import via `useAuthStore(selector)` or `useWorkspaceStore(selector)`.
- Stores must not call `useRouter` or any React hooks — keep navigation in components.
- Cross-store reads use `useOtherStore.getState()` inside actions (not hooks).
- Dependency direction: `workspace``auth`, `realtime``auth`, `issues``workspace`. Never reverse.
### Import Aliases
Use `@/` alias (maps to `apps/web/`):
```typescript
import { api } from "@/shared/api";
import type { Issue } from "@/shared/types";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useIssueStore } from "@/features/issues";
import { useInboxStore } from "@/features/inbox";
import { useWSEvent } from "@/features/realtime";
import { StatusIcon } from "@/features/issues/components";
```
Within a feature, use relative imports. Between features or to shared, use `@/`.
### Data Flow
```
Browser → ApiClient (shared/api) → REST API (Chi handlers) → sqlc queries → PostgreSQL
Browser ← WSClient (shared/api) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskService
```
### Backend Structure (`server/`)
- **Entry points** (`cmd/`): `server` (HTTP API), `multica` (CLI — daemon, agent management, config), `migrate`
- **Handlers** (`internal/handler/`): One file per domain (issue, comment, agent, auth, daemon, etc.). Each handler holds `Queries`, `DB`, `Hub`, and `TaskService`.
- **Real-time** (`internal/realtime/`): Hub manages WebSocket clients. Server broadcasts events; inbound WS message routing is still TODO.
- **Auth** (`internal/auth/` + `internal/middleware/`): JWT (HS256). Middleware sets `X-User-ID` and `X-User-Email` headers. Login creates user on-the-fly if not found.
- **Task lifecycle** (`internal/service/task.go`): Orchestrates agent work — enqueue → claim → start → complete/fail. Syncs issue status automatically and broadcasts WS events at each transition.
- **Agent SDK** (`pkg/agent/`): Unified `Backend` interface for executing prompts via Claude Code or Codex. Each backend spawns its CLI and streams results via `Session.Messages` + `Session.Result` channels.
- **Daemon** (`internal/daemon/`): Local agent runtime — auto-detects available CLIs (claude, codex), registers runtimes, polls for tasks, routes by provider.
- **CLI** (`internal/cli/`): Shared helpers for the `multica` CLI — API client, config management, output formatting.
- **Events** (`internal/events/`): Internal event bus for decoupled communication between handlers and services.
- **Logging** (`internal/logger/`): Structured logging via slog. `LOG_LEVEL` env var controls level (debug, info, warn, error).
- **Database**: PostgreSQL with pgvector extension (`pgvector/pgvector:pg17`). sqlc generates Go code from SQL in `pkg/db/queries/``pkg/db/generated/`. Migrations in `migrations/`.
- **Routes** (`cmd/server/router.go`): Public routes (auth, health, ws) + protected routes (require JWT) + daemon routes (unauthenticated, separate auth model).
### Multi-tenancy
All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace.
### Agent Assignees
Assignees are polymorphic — can be a member or an agent. `assignee_type` + `assignee_id` on issues. Agents render with distinct styling (purple background, robot icon).
## Commands
```bash
# One-click setup & run
make setup # First-time: ensure shared DB, create app DB, migrate
make start # Start backend + frontend together
make stop # Stop app processes for the current checkout
make db-down # Stop the shared PostgreSQL container
# Frontend
pnpm install
pnpm dev:web # Next.js dev server (port 3000)
pnpm build # Build frontend
pnpm typecheck # TypeScript check
pnpm lint # ESLint via Next.js
pnpm test # TS tests (Vitest)
# Backend (Go)
make dev # Run Go server (port 8080)
make daemon # Run local daemon
make build # Build server + CLI binaries to server/bin/
make cli ARGS="..." # Run multica CLI (e.g. make cli ARGS="config")
make test # Go tests
make sqlc # Regenerate sqlc code after editing SQL in server/pkg/db/queries/
make migrate-up # Run database migrations
make migrate-down # Rollback migrations
# Run a single Go test
cd server && go test ./internal/handler/ -run TestName
# Run a single TS test
pnpm --filter @multica/web exec vitest run src/path/to/file.test.ts
# Run a single E2E test (requires backend + frontend running)
pnpm exec playwright test e2e/tests/specific-test.spec.ts
# Infrastructure
make db-up # Start shared PostgreSQL (pgvector/pg17 image)
make db-down # Stop shared PostgreSQL
```
### CI Requirements
CI runs on Node 22 and Go 1.24 with a `pgvector/pgvector:pg17` PostgreSQL service. See `.github/workflows/ci.yml`.
### Worktree Support
All checkouts share one PostgreSQL container. Isolation is at the database level — each worktree gets its own DB name and unique ports via `.env.worktree`. Main checkouts use `.env`.
```bash
make worktree-env # Generate .env.worktree with unique DB/ports
make setup-worktree # Setup using .env.worktree
make start-worktree # Start using .env.worktree
```
## Coding Rules
- TypeScript strict mode is enabled; keep types explicit.
- TypeScript in `apps/web` uses 2-space indentation, double quotes, and semicolons.
- Prefer PascalCase for React components, camelCase for hooks and helpers, and colocated test files such as `page.test.tsx`.
- Go code follows standard Go conventions (gofmt, go vet). Use domain-oriented filenames like `issue.go` or `cmd_issue.go`.
- Do not hand-edit generated code in `server/pkg/db/generated/`.
- Keep comments in code **English only**.
- Prefer existing patterns/components over introducing parallel abstractions.
- Unless the user explicitly asks for backwards compatibility, do **not** add compatibility layers, fallback paths, dual-write logic, legacy adapters, or temporary shims.
- If a flow or API is being replaced and the product is not yet live, prefer removing the old path instead of preserving both old and new behavior.
- Treat compatibility code as a maintenance cost, not a default safety mechanism. Avoid "just in case" branches that make the codebase harder to reason about.
- Avoid broad refactors unless required by the task.
## UI/UX Rules
- Prefer shadcn components over custom implementations. Install missing components via `npx shadcn add`.
- **Feature-specific components** → `features/<domain>/components/` — issue icons, pickers, and other domain-bound UI live inside their feature module.
- Use shadcn design tokens for styling (e.g. `bg-primary`, `text-muted-foreground`, `text-destructive`). Avoid hardcoded color values (e.g. `text-red-500`, `bg-gray-100`).
- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design. Prefer zustand stores for shared state over React Context.
- Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency.
- When unsure about interaction or state design, ask — the user will provide direction.
## Testing Rules
- **TypeScript**: Vitest with Testing Library. Shared test setup lives in `apps/web/test/`. Mock external/third-party dependencies only.
- **Go**: Standard `go test`. Tests should create their own fixture data in a test database.
- End-to-end tests live in `e2e/*.spec.ts`; `make check` will start missing services automatically, while direct Playwright runs expect the app to already be running.
- Add or update tests whenever you change handlers, CLI commands, daemon behavior, or SQL-backed flows.
## Commit & Pull Request Rules
- Use atomic commits grouped by logical intent.
- Conventional format with scopes:
- `feat(web): ...`, `feat(cli): ...`
- `fix(web): ...`, `fix(cli): ...`
- `refactor(daemon): ...`
- `test(cli): ...`
- `docs: ...`
- `chore(scope): ...`
- Keep PRs focused and include a short description, linked issue or PR number when relevant, screenshots for UI work, and notes for migrations, env changes, or CLI surface changes.
- Before opening a PR, run `make check` or the relevant frontend/backend subset.
## Minimum Pre-Push Checks
```bash
make check # Runs all checks: typecheck, unit tests, Go tests, E2E
```
Run verification only when the user explicitly asks for it.
For targeted checks when requested:
```bash
pnpm typecheck # TypeScript type errors only
pnpm test # TS unit tests only (Vitest)
make test # Go tests only
pnpm exec playwright test # E2E only (requires backend + frontend running)
```
## AI Agent Verification Loop
After writing or modifying code, always run the full verification pipeline:
```bash
make check
```
This runs all checks in sequence:
1. TypeScript typecheck (`pnpm typecheck`)
2. TypeScript unit tests (`pnpm test`)
3. Go tests (`go test ./...`)
4. E2E tests (auto-starts backend + frontend if needed, runs Playwright)
**Workflow:**
- Write code to satisfy the requirement
- Run `make check`
- If any step fails, read the error output, fix the code, and re-run `make check`
- Repeat until all checks pass
- Only then consider the task complete
**Quick iteration:** If you know only TypeScript or Go is affected, run individual checks first for faster feedback, then finish with a full `make check` before marking work complete.
## E2E Test Patterns
E2E tests should be self-contained. Use the `TestApiClient` fixture for data setup/teardown:
```typescript
import { loginAsDefault, createTestApi } from "./helpers";
import type { TestApiClient } from "./fixtures";
let api: TestApiClient;
test.beforeEach(async ({ page }) => {
api = await createTestApi(); // logged-in API client
await loginAsDefault(page); // browser session
});
test.afterEach(async () => {
await api.cleanup(); // delete any data created during the test
});
test("example", async ({ page }) => {
const issue = await api.createIssue("Test Issue"); // create via API
await page.goto(`/issues/${issue.id}`); // test via UI
// api.cleanup() in afterEach removes the issue
});
```

316
CLAUDE.md
View File

@@ -12,140 +12,112 @@ Multica is an AI-native task management platform — like Linear, but with AI ag
## Architecture
**Polyglot monorepo** — Go backend + TypeScript frontend.
**Go backend + monorepo frontend (pnpm workspaces + Turborepo) with shared packages.**
- `server/` — Go backend (Chi router, sqlc for DB, gorilla/websocket for real-time)
- `apps/web/` — Next.js 16 frontend (App Router)
- `packages/` — Shared TypeScript packages (ui, types, sdk, store, hooks, utils)
- `apps/web/` — Next.js frontend (App Router)
- `apps/desktop/` — Electron desktop app (electron-vite)
- `packages/core/` — Headless business logic (zero react-dom, all-platform reuse)
- `packages/ui/` — Atomic UI components (zero business logic)
- `packages/views/` — Shared business pages/components (zero next/* imports, zero react-router imports)
- `packages/tsconfig/` — Shared TypeScript configuration
### Web App Structure (`apps/web/`)
### Key Architectural Decisions
The frontend uses a **feature-based architecture** with three layers:
**Internal Packages pattern** — all shared packages export raw `.ts`/`.tsx` files (no pre-compilation). The consuming app's bundler compiles them directly. This gives zero-config HMR and instant go-to-definition.
```
apps/web/
├── app/ # Routing layer (thin shells — import from features/)
├── features/ # Business logic, organized by domain
├── shared/ # Cross-feature utilities (api client)
```
**Dependency direction:** `views/ → core/ + ui/`. Core and UI are independent of each other. No package imports from `next/*`, `react-router-dom`, or app-specific code.
**`app/`** — Next.js App Router pages. Route files should be thin: import and re-export from `features/`. Layout components and route-specific glue (redirects, auth guards) live here. Shared layout components (e.g. `app-sidebar`) stay in `app/(dashboard)/_components/`.
**Platform bridge:** `packages/core/platform/` provides `CoreProvider` — initializes API client, auth/workspace stores, WS connection, and QueryClient. Each app wraps its root with `<CoreProvider>` and provides its own `NavigationAdapter` for routing.
**`features/`** — Domain modules, each with its own components, hooks, stores, and config:
| Feature | Purpose | Exports |
|---|---|---|
| `features/auth/` | Authentication state | `useAuthStore`, `AuthInitializer` |
| `features/workspace/` | Workspace, members, agents | `useWorkspaceStore`, `useActorName` |
| `features/issues/` | Issue state, components, config | `useIssueStore`, icons, pickers, status/priority config |
| `features/inbox/` | Inbox notification state | `useInboxStore` |
| `features/realtime/` | WebSocket connection + sync | `WSProvider`, `useWSEvent`, `useRealtimeSync` |
**`shared/`** — Code used across multiple features. Currently only `api.ts` (SDK singleton).
**pnpm catalog**`pnpm-workspace.yaml` defines `catalog:` for version pinning. All shared deps use `catalog:` references to guarantee a single version across all packages. When adding new shared deps (including test deps), add to catalog first.
### State Management
- **Zustand** for global client state — one store per feature domain (`features/auth/store.ts`, `features/workspace/store.ts`, `features/issues/store.ts`, `features/inbox/store.ts`).
- **React Context** only for connection lifecycle (`WSProvider` in `features/realtime/`).
- **Local `useState`** for component-scoped UI state (forms, modals, filters).
- Do not use React Context for data that can be a zustand store.
The architecture relies on a strict split between server state and client state. Mixing them is the most common way to break it.
**Store conventions:**
- One store per feature domain. Import via `useAuthStore(selector)` or `useWorkspaceStore(selector)`.
- Stores must not call `useRouter` or any React hooks — keep navigation in components.
- Cross-store reads use `useOtherStore.getState()` inside actions (not hooks).
- Dependency direction: `workspace``auth`, `realtime``auth`, `issues``workspace`. Never reverse.
- **TanStack Query owns all server state.** Issues, users, workspaces, inbox — anything fetched from the API lives in the Query cache. WS events keep it fresh via invalidation; no polling, no `staleTime` workarounds.
- **Zustand owns all client state.** UI selections, filters, drafts, modal state, navigation history. Stores live in `packages/core/` (never in `packages/views/`) so both apps share them.
- **React Context** is reserved for cross-cutting platform plumbing — `WorkspaceIdProvider`, `NavigationProvider`. Don't reach for it for general state.
- **Auth and workspace stores are the only stores allowed to call `api.*` directly**, because they manage critical state that must exist before queries can run. They're created via factory + injected dependencies, registered by the platform layer.
### Import Aliases
**Hard rules — these are how the architecture stays coherent:**
Use `@/` alias (maps to `apps/web/`):
```typescript
import { api } from "@/shared/api";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useIssueStore } from "@/features/issues";
import { useInboxStore } from "@/features/inbox";
import { useWSEvent } from "@/features/realtime";
import { StatusIcon } from "@/features/issues/components";
```
- **Never duplicate server data into Zustand.** If it came from the API, it belongs in the Query cache. Copying it into a store creates two sources of truth and they will drift.
- **Workspace-scoped queries must key on `wsId`.** This is what makes workspace switching automatic — the cache key changes, the right data appears, no manual invalidation needed.
- **Mutations are optimistic by default.** Apply the change locally, send the request, roll back on failure, invalidate on settle. The user shouldn't wait for the server.
- **WS events invalidate queries — they never write to stores directly.** This keeps the cache as the single source of truth and avoids race conditions.
- **Persist what's worth preserving across restarts** (user preferences, drafts, tab layout). **Don't persist ephemeral UI state** (modal open/close, transient selections) or server data.
Within a feature, use relative imports. Between features or to shared, use `@/`.
**Common Zustand footguns to avoid:**
### Data Flow
```
Browser → ApiClient (SDK) → REST API (Chi handlers) → sqlc queries → PostgreSQL
Browser ← WSClient (SDK) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskService
```
### Backend Structure (`server/`)
- **Entry points** (`cmd/`): `server` (HTTP API), `multica` (CLI — daemon, agent management, config), `migrate`
- **Handlers** (`internal/handler/`): One file per domain (issue, comment, agent, auth, daemon, etc.). Each handler holds `Queries`, `DB`, `Hub`, and `TaskService`.
- **Real-time** (`internal/realtime/`): Hub manages WebSocket clients. Server broadcasts events; inbound WS message routing is still TODO.
- **Auth** (`internal/auth/` + `internal/middleware/`): JWT (HS256). Middleware sets `X-User-ID` and `X-User-Email` headers. Login creates user on-the-fly if not found.
- **Task lifecycle** (`internal/service/task.go`): Orchestrates agent work — enqueue → claim → start → complete/fail. Syncs issue status automatically and broadcasts WS events at each transition.
- **Agent SDK** (`pkg/agent/`): Unified `Backend` interface for executing prompts via Claude Code or Codex. Each backend spawns its CLI and streams results via `Session.Messages` + `Session.Result` channels.
- **Daemon** (`internal/daemon/`): Local agent runtime — auto-detects available CLIs (claude, codex), registers runtimes, polls for tasks, routes by provider.
- **CLI** (`internal/cli/`): Shared helpers for the `multica` CLI — API client, config management, output formatting.
- **Database**: sqlc generates Go code from SQL in `pkg/db/queries/``pkg/db/generated/`. Migrations in `migrations/`.
- **Routes** (`cmd/server/router.go`): Public routes (auth, health, ws) + protected routes (require JWT) + daemon routes (unauthenticated, separate auth model).
### Key Packages
- **`@multica/sdk`**: `ApiClient` (REST) and `WSClient` (WebSocket) classes. All backend communication goes through here.
- **`@multica/types`**: Shared domain types + WebSocket event types (issue:created/updated/deleted, task:*, agent:status, comment:*, inbox:new, daemon:*).
- **`@multica/ui`**: shadcn/ui component library with Radix primitives, Tailwind CSS 4, Shiki syntax highlighting for markdown.
### Multi-tenancy
All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace.
### Agent Assignees
Assignees are polymorphic — can be a member or an agent. `assignee_type` + `assignee_id` on issues. Agents render with distinct styling (purple background, robot icon).
- Selectors must return stable references. Returning a freshly built object or array on every call (e.g. `s => ({ a: s.a, b: s.b })` or `s => s.items.map(...)`) triggers infinite re-renders. Either select primitives separately or use shallow comparison.
- Hooks that need workspace context should accept `wsId` as a parameter, not call `useWorkspaceId()` internally — this lets them work outside the `WorkspaceIdProvider` (e.g. in a sidebar that renders before workspace is loaded).
## Commands
```bash
# One-click setup & run
# One-command dev (auto-setup + start everything)
make dev # Auto-creates env, installs deps, starts DB, migrates, launches app
# Explicit setup & run (if you prefer separate steps)
make setup # First-time: ensure shared DB, create app DB, migrate
make start # Start backend + frontend together
make stop # Stop app processes for the current checkout
make db-down # Stop the shared PostgreSQL container
# Frontend
# Frontend (all commands go through Turborepo)
pnpm install
pnpm dev:web # Next.js dev server (port 3000)
pnpm build # Build all TS packages
pnpm typecheck # TypeScript check
pnpm test # TS tests (Vitest)
pnpm dev:desktop # Electron dev (electron-vite, HMR)
pnpm build # Build all frontend apps
pnpm typecheck # TypeScript check (all packages + apps via turbo)
pnpm lint # ESLint
pnpm test # TS tests (Vitest, all packages + apps via turbo)
# Backend (Go)
make dev # Run Go server (port 8080)
make server # Run Go server only (port 8080)
make daemon # Run local daemon
make build # Build server + CLI binaries to server/bin/
make cli ARGS="..." # Run multica CLI (e.g. make cli ARGS="config")
make test # Go tests
make sqlc # Regenerate sqlc code after editing SQL in server/pkg/db/queries/
make migrate-up # Run database migrations
make migrate-down # Rollback migrations
# Run a single TS test (works for any package with a test script)
pnpm --filter @multica/views exec vitest run auth/login-page.test.tsx
pnpm --filter @multica/core exec vitest run runtimes/version.test.ts
pnpm --filter @multica/web exec vitest run app/\(auth\)/login/page.test.tsx
# Run a single Go test
cd server && go test ./internal/handler/ -run TestName
# Run a single TS test
pnpm --filter @multica/web exec vitest run src/path/to/file.test.ts
# Run a single E2E test (requires backend + frontend running)
pnpm exec playwright test e2e/tests/specific-test.spec.ts
# Desktop build & package
pnpm --filter @multica/desktop build # Compile TS → JS (reads .env.production)
pnpm --filter @multica/desktop package # Package into .app/.dmg/.exe (current platform only)
# shadcn — config lives in packages/ui/components.json (Base UI variant, base-nova style)
pnpm ui:add badge # Adds component to packages/ui/components/ui/
# Infrastructure
make db-up # Start shared PostgreSQL
make db-up # Start shared PostgreSQL (pgvector/pg17 image)
make db-down # Stop shared PostgreSQL
```
### CI Requirements
CI runs on Node 22 and Go 1.26.1 with a `pgvector/pgvector:pg17` PostgreSQL service. See `.github/workflows/ci.yml`.
### Worktree Support
For isolated feature testing with a separate database:
All checkouts share one PostgreSQL container. Isolation is at the database level — each worktree gets its own DB name and unique ports via `.env.worktree`. Main checkouts use `.env`.
`make dev` auto-detects worktrees and handles everything. For explicit control:
```bash
make worktree-env # Generate .env.worktree with unique DB/ports
make setup-worktree # Setup using .env.worktree
@@ -160,35 +132,129 @@ make start-worktree # Start using .env.worktree
- Prefer existing patterns/components over introducing parallel abstractions.
- Unless the user explicitly asks for backwards compatibility, do **not** add compatibility layers, fallback paths, dual-write logic, legacy adapters, or temporary shims.
- If a flow or API is being replaced and the product is not yet live, prefer removing the old path instead of preserving both old and new behavior.
- Treat compatibility code as a maintenance cost, not a default safety mechanism. Avoid "just in case" branches that make the codebase harder to reason about.
- Avoid broad refactors unless required by the task.
### Package Boundary Rules
These are hard constraints. Violating them breaks the cross-platform architecture:
- `packages/core/` — zero react-dom, zero localStorage (use StorageAdapter), zero process.env, zero UI libraries. **All shared Zustand stores live here**, even view-related ones (filters, view modes) — stores are pure state, not UI.
- `packages/ui/` — zero `@multica/core` imports (pure UI, no business logic).
- `packages/views/` — zero `next/*` imports, zero `react-router-dom` imports, zero stores. Use `NavigationAdapter` for all routing.
- `apps/web/platform/` — the only place for Next.js APIs (`next/navigation`).
- `apps/desktop/src/renderer/src/platform/` — the only place for react-router-dom navigation wiring.
### The No-Duplication Rule
**If the same logic exists in both apps, it must be extracted to a shared package.**
This applies to everything: components, hooks, guards, providers, utility functions. The decision process:
1. Does this code depend on Next.js or Electron APIs? → Keep in the respective app.
2. Does it depend on `react-router-dom` or `next/navigation`? → Keep in app's `platform/` layer.
3. Everything else → belongs in `packages/core/` (headless logic) or `packages/views/` (UI components).
When the two apps need different behavior for the same concept (e.g., different loading UI), extract the shared logic into a component with props/slots for the differences. Don't duplicate the logic.
### Cross-Platform Development Rules
When adding a new page or feature:
1. **New page component** → add to `packages/views/<domain>/`. Never import from `next/*` or `react-router-dom`.
2. **Wire it in both apps** → add a route in `apps/web/app/` (Next.js page file) AND in the desktop router.
3. **Navigation** → use `useNavigation().push()` or `<AppLink>`. Never use framework-specific link/router APIs in shared code.
4. **Shared guards/providers** → use `DashboardGuard` from `packages/views/layout/`. Don't create separate guard logic per app.
5. **Platform-specific UI** → if a feature is web-only or desktop-only, keep it in the respective app. Use props slots (`extra`, `topSlot`) on shared layout components to inject platform-specific UI.
6. **New hooks that need workspace context** → accept `wsId` as parameter instead of reading from `useWorkspaceId()` Context, so they work both inside and outside `WorkspaceIdProvider`.
### CSS Architecture
Both apps share the same CSS foundation from `packages/ui/styles/`.
- **Design tokens** → use semantic tokens (`bg-background`, `text-muted-foreground`). Never use hardcoded Tailwind colors (`text-red-500`, `bg-gray-100`).
- **Shared styles** → `packages/ui/styles/`. Never duplicate scrollbar styling, keyframes, or base layer rules in app CSS.
- **`@source` directives** → both apps scan shared packages so Tailwind sees all class names.
## UI/UX Rules
- Prefer `packages/ui` shadcn components over custom implementations.
- **shadcn official components** → `packages/ui/src/components/ui/` — keep this directory clean; install missing components via `npx shadcn add`, do not mix in business code.
- **Shared business components & utils** → `packages/ui/src/components/common/` — reusable project-level UI components (e.g. ActorAvatar) and shared utilities live here.
- **Feature-specific components** → `features/<domain>/components/` — issue icons, pickers, and other domain-bound UI live inside their feature module, not in `packages/ui`.
- Use shadcn design tokens for styling (e.g. `bg-primary`, `text-muted-foreground`, `text-destructive`). Avoid hardcoded color values (e.g. `text-red-500`, `bg-gray-100`).
- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design. Prefer zustand stores for shared state over React Context.
- Prefer shadcn components over custom implementations. Install via `pnpm ui:add <component>` from project root — adds to `packages/ui/components/ui/`. All components use Base UI primitives (`@base-ui/react`), not Radix.
- Use shadcn design tokens for styling. Avoid hardcoded color values.
- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design.
- Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency.
- When unsure about interaction or state design, ask — the user will provide direction.
- **If a component is identical between web and desktop, it belongs in a shared package.** Do not copy-paste between apps.
## Testing Rules
- **TypeScript**: Vitest. Mock external/third-party dependencies only.
- **Go**: Standard `go test`. Tests should create their own fixture data in a test database.
### Where to write tests
Tests follow the code, not the app. This is the most important testing principle in this monorepo:
| What you're testing | Where the test lives | Why |
|---|---|---|
| Shared business logic (stores, queries, hooks) | `packages/core/*.test.ts` | No DOM needed, pure logic |
| Shared UI components (pages, forms, modals) | `packages/views/*.test.tsx` | jsdom, no framework mocks |
| Platform-specific wiring (cookies, redirects, searchParams) | `apps/web/*.test.tsx` or `apps/desktop/` | Needs framework-specific mocks |
| End-to-end user flows | `e2e/*.spec.ts` | Real browser, real backend |
**Never test shared component behavior in an app's test file.** If a test requires mocking `next/navigation` or `react-router-dom` to test a component from `@multica/views`, the test is in the wrong place — move it to `packages/views/` and mock `@multica/core` instead.
### Test infrastructure
- `packages/core/` — Vitest, Node environment (no DOM)
- `packages/views/` — Vitest, jsdom environment, `@testing-library/react`
- `apps/web/` — Vitest, jsdom environment, framework-specific mocks
- `e2e/` — Playwright
- `server/` — Go standard `go test`
All test deps are in the pnpm catalog for unified versioning.
### Mocking conventions
- Mock `@multica/core` stores with `vi.hoisted()` + `Object.assign(selectorFn, { getState })` pattern (Zustand stores are both callable and have `.getState()`).
- Mock `@multica/core/api` for API calls.
- In `packages/views/` tests: never mock `next/*` or `react-router-dom` — those don't exist here.
- In `apps/web/` tests: mock framework-specific APIs only for platform-specific behavior.
### TDD workflow
1. Write failing test in the **correct package** first.
2. Write implementation.
3. Run `pnpm test` (Turborepo discovers all packages).
4. Green → done.
### Go tests
Standard `go test`. Tests should create their own fixture data in a test database.
### E2E tests
E2E tests should be self-contained. Use the `TestApiClient` fixture for data setup/teardown:
```typescript
import { loginAsDefault, createTestApi } from "./helpers";
import type { TestApiClient } from "./fixtures";
let api: TestApiClient;
test.beforeEach(async ({ page }) => {
api = await createTestApi();
await loginAsDefault(page);
});
test.afterEach(async () => {
await api.cleanup();
});
test("example", async ({ page }) => {
const issue = await api.createIssue("Test Issue");
await page.goto(`/issues/${issue.id}`);
});
```
## Commit Rules
- Use atomic commits grouped by logical intent.
- Conventional format:
- `feat(scope): ...`
- `fix(scope): ...`
- `refactor(scope): ...`
- `docs: ...`
- `test(scope): ...`
- `chore(scope): ...`
- Conventional format: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`.
## Minimum Pre-Push Checks
@@ -201,7 +267,7 @@ Run verification only when the user explicitly asks for it.
For targeted checks when requested:
```bash
pnpm typecheck # TypeScript type errors only
pnpm test # TS unit tests only (Vitest)
pnpm test # TS unit tests only (Vitest, all packages)
make test # Go tests only
pnpm exec playwright test # E2E only (requires backend + frontend running)
```
@@ -214,43 +280,29 @@ After writing or modifying code, always run the full verification pipeline:
make check
```
This runs all checks in sequence:
1. TypeScript typecheck (`pnpm typecheck`)
2. TypeScript unit tests (`pnpm test`)
3. Go tests (`go test ./...`)
4. E2E tests (auto-starts backend + frontend if needed, runs Playwright)
**Workflow:**
- Write code to satisfy the requirement
- Run `make check`
- If any step fails, read the error output, fix the code, and re-run `make check`
- If any step fails, read the error output, fix the code, and re-run
- Repeat until all checks pass
- Only then consider the task complete
**Quick iteration:** If you know only TypeScript or Go is affected, run individual checks first for faster feedback, then finish with a full `make check` before marking work complete.
## E2E Test Patterns
## CLI Release
E2E tests should be self-contained. Use the `TestApiClient` fixture for data setup/teardown:
**Prerequisite:** A CLI release must accompany every Production deployment.
```typescript
import { loginAsDefault, createTestApi } from "./helpers";
import type { TestApiClient } from "./fixtures";
1. Create a tag on the `main` branch: `git tag v0.x.x`
2. Push the tag: `git push origin v0.x.x`
3. GitHub Actions automatically triggers `release.yml`: runs Go tests → GoReleaser builds multi-platform binaries → publishes to GitHub Releases + Homebrew tap
let api: TestApiClient;
By default, bump the patch version each release (e.g. `v0.1.12``v0.1.13`), unless the user specifies a specific version.
test.beforeEach(async ({ page }) => {
api = await createTestApi(); // logged-in API client
await loginAsDefault(page); // browser session
});
## Multi-tenancy
test.afterEach(async () => {
await api.cleanup(); // delete any data created during the test
});
All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace.
test("example", async ({ page }) => {
const issue = await api.createIssue("Test Issue"); // create via API
await page.goto(`/issues/${issue.id}`); // test via UI
// api.cleanup() in afterEach removes the issue
});
```
## Agent Assignees
Assignees are polymorphic — can be a member or an agent. `assignee_type` + `assignee_id` on issues. Agents render with distinct styling (purple background, robot icon).

387
CLI_AND_DAEMON.md Normal file
View File

@@ -0,0 +1,387 @@
# CLI and Agent Daemon Guide
The `multica` CLI connects your local machine to Multica. It handles authentication, workspace management, issue tracking, and runs the agent daemon that executes AI tasks locally.
## Installation
### Homebrew (macOS/Linux)
```bash
brew tap multica-ai/tap
brew install multica
```
### Build from Source
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make build
cp server/bin/multica /usr/local/bin/multica
```
### Update
```bash
multica update
```
This auto-detects your installation method (Homebrew or manual) and upgrades accordingly.
## Quick Start
```bash
# One-command setup: configure, authenticate, and start the daemon
multica setup
# For self-hosted (local) deployments:
multica setup --local
```
Or step by step:
```bash
# 1. Authenticate (opens browser for login)
multica login
# 2. Start the agent daemon
multica daemon start
# 3. Done — agents in your watched workspaces can now execute tasks on your machine
```
`multica login` automatically discovers all workspaces you belong to and adds them to the daemon watch list.
## Authentication
### Browser Login
```bash
multica login
```
Opens your browser for OAuth authentication, creates a 90-day personal access token, and auto-configures your workspaces.
### Token Login
```bash
multica login --token
```
Authenticate by pasting a personal access token directly. Useful for headless environments.
### Check Status
```bash
multica auth status
```
Shows your current server, user, and token validity.
### Logout
```bash
multica auth logout
```
Removes the stored authentication token.
## Agent Daemon
The daemon is the local agent runtime. It detects available AI CLIs on your machine, registers them with the Multica server, and executes tasks when agents are assigned work.
### Start
```bash
multica daemon start
```
By default, the daemon runs in the background and logs to `~/.multica/daemon.log`.
To run in the foreground (useful for debugging):
```bash
multica daemon start --foreground
```
### Stop
```bash
multica daemon stop
```
### Status
```bash
multica daemon status
multica daemon status --output json
```
Shows PID, uptime, detected agents, and watched workspaces.
### Logs
```bash
multica daemon logs # Last 50 lines
multica daemon logs -f # Follow (tail -f)
multica daemon logs -n 100 # Last 100 lines
```
### Supported Agents
The daemon auto-detects these AI CLIs on your PATH:
| CLI | Command | Description |
|-----|---------|-------------|
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `claude` | Anthropic's coding agent |
| [Codex](https://github.com/openai/codex) | `codex` | OpenAI's coding agent |
You need at least one installed. The daemon registers each detected CLI as an available runtime.
### How It Works
1. On start, the daemon detects installed agent CLIs and registers a runtime for each agent in each watched workspace
2. It polls the server at a configurable interval (default: 3s) for claimed tasks
3. When a task arrives, it creates an isolated workspace directory, spawns the agent CLI, and streams results back
4. Heartbeats are sent periodically (default: 15s) so the server knows the daemon is alive
5. On shutdown, all runtimes are deregistered
### Configuration
Daemon behavior is configured via flags or environment variables:
| Setting | Flag | Env Variable | Default |
|---------|------|--------------|---------|
| Poll interval | `--poll-interval` | `MULTICA_DAEMON_POLL_INTERVAL` | `3s` |
| Heartbeat interval | `--heartbeat-interval` | `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` |
| Agent timeout | `--agent-timeout` | `MULTICA_AGENT_TIMEOUT` | `2h` |
| Max concurrent tasks | `--max-concurrent-tasks` | `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` |
| Daemon ID | `--daemon-id` | `MULTICA_DAEMON_ID` | hostname |
| Device name | `--device-name` | `MULTICA_DAEMON_DEVICE_NAME` | hostname |
| Runtime name | `--runtime-name` | `MULTICA_AGENT_RUNTIME_NAME` | `Local Agent` |
| Workspaces root | — | `MULTICA_WORKSPACES_ROOT` | `~/multica_workspaces` |
Agent-specific overrides:
| Variable | Description |
|----------|-------------|
| `MULTICA_CLAUDE_PATH` | Custom path to the `claude` binary |
| `MULTICA_CLAUDE_MODEL` | Override the Claude model used |
| `MULTICA_CODEX_PATH` | Custom path to the `codex` binary |
| `MULTICA_CODEX_MODEL` | Override the Codex model used |
### Self-Hosted Server
When connecting to a self-hosted Multica instance, the easiest approach is:
```bash
# One command — auto-detects local server, configures, authenticates, starts daemon
multica setup --local
```
Or configure manually:
```bash
# Configure for local Docker Compose (default ports)
multica config local
# Or set URLs individually:
# multica config set app_url http://localhost:3000
# multica config set server_url http://localhost:8080
# For production with TLS:
# multica config set app_url https://app.example.com
# multica config set server_url https://api.example.com
multica login
multica daemon start
```
### Profiles
Profiles let you run multiple daemons on the same machine — for example, one for production and one for a staging server.
```bash
# Start a daemon for the staging server
multica --profile staging login
multica --profile staging daemon start
# Default profile runs separately
multica daemon start
```
Each profile gets its own config directory (`~/.multica/profiles/<name>/`), daemon state, health port, and workspace root.
## Workspaces
### List Workspaces
```bash
multica workspace list
```
Watched workspaces are marked with `*`. The daemon only processes tasks for watched workspaces.
### Watch / Unwatch
```bash
multica workspace watch <workspace-id>
multica workspace unwatch <workspace-id>
```
### Get Details
```bash
multica workspace get <workspace-id>
multica workspace get <workspace-id> --output json
```
### List Members
```bash
multica workspace members <workspace-id>
```
## Issues
### List Issues
```bash
multica issue list
multica issue list --status in_progress
multica issue list --priority urgent --assignee "Agent Name"
multica issue list --limit 20 --output json
```
Available filters: `--status`, `--priority`, `--assignee`, `--limit`.
### Get Issue
```bash
multica issue get <id>
multica issue get <id> --output json
```
### Create Issue
```bash
multica issue create --title "Fix login bug" --description "..." --priority high --assignee "Lambda"
```
Flags: `--title` (required), `--description`, `--status`, `--priority`, `--assignee`, `--parent`, `--due-date`.
### Update Issue
```bash
multica issue update <id> --title "New title" --priority urgent
```
### Assign Issue
```bash
multica issue assign <id> --to "Lambda"
multica issue assign <id> --unassign
```
### Change Status
```bash
multica issue status <id> in_progress
```
Valid statuses: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`.
### Comments
```bash
# List comments
multica issue comment list <issue-id>
# Add a comment
multica issue comment add <issue-id> --content "Looks good, merging now"
# Reply to a specific comment
multica issue comment add <issue-id> --parent <comment-id> --content "Thanks!"
# Delete a comment
multica issue comment delete <comment-id>
```
### Execution History
```bash
# List all execution runs for an issue
multica issue runs <issue-id>
multica issue runs <issue-id> --output json
# View messages for a specific execution run
multica issue run-messages <task-id>
multica issue run-messages <task-id> --output json
# Incremental fetch (only messages after a given sequence number)
multica issue run-messages <task-id> --since 42 --output json
```
The `runs` command shows all past and current executions for an issue, including running tasks. The `run-messages` command shows the detailed message log (tool calls, thinking, text, errors) for a single run. Use `--since` for efficient polling of in-progress runs.
## Setup
```bash
# One-command setup: configure, authenticate, and start the daemon
multica setup
# For local self-hosted deployments (auto-detects or forces local mode)
multica setup --local
# Custom ports
multica setup --local --port 9090 --frontend-port 4000
```
`multica setup` detects whether a local Multica server is running, configures the CLI, opens your browser for authentication, and starts the daemon — all in one step.
## Configuration
### View Config
```bash
multica config show
```
Shows config file path, server URL, app URL, and default workspace.
### Configure for Local Self-Hosted
```bash
multica config local # Uses default ports (8080/3000)
multica config local --port 9090 --frontend-port 4000 # Custom ports
```
Sets `server_url` and `app_url` for a local Docker Compose deployment in one command.
### Set Values
```bash
multica config set server_url https://api.example.com
multica config set app_url https://app.example.com
multica config set workspace_id <workspace-id>
```
## Other Commands
```bash
multica version # Show CLI version and commit hash
multica update # Update to latest version
multica agent list # List agents in the current workspace
```
## Output Formats
Most commands support `--output` with two formats:
- `table` — human-readable table (default for list commands)
- `json` — structured JSON (useful for scripting and automation)
```bash
multica issue list --output json
multica daemon status --output json
```

171
CLI_INSTALL.md Normal file
View File

@@ -0,0 +1,171 @@
# Multica CLI — Installation Guide for AI Agents
> **Audience:** This document is designed for AI agents (Claude Code, Codex, etc.) to read and execute step-by-step. Each step includes the exact shell commands to run, expected outcomes, and error handling.
## One-Line Instruction for Users
Give this instruction to your AI agent:
```
Fetch https://github.com/multica-ai/multica/blob/main/CLI_INSTALL.md and follow the instructions to install Multica CLI, log in, and start the daemon on this machine.
```
---
## Step 1: Check if Multica CLI is already installed
Run:
```bash
multica version
```
- **If it prints a version string** (e.g. `multica v0.x.x`): skip to **Step 3**.
- **If command not found**: continue to **Step 2**.
---
## Step 2: Install the Multica CLI
### Option A: Homebrew (preferred)
Check if Homebrew is available:
```bash
which brew
```
If `brew` is found, install via Homebrew:
```bash
brew tap multica-ai/tap && brew install multica
```
Then verify:
```bash
multica version
```
If the version prints successfully, skip to **Step 3**.
### Option B: Download from GitHub Releases (no Homebrew)
If Homebrew is not available, download the binary directly.
Detect OS and architecture, then download the correct archive:
```bash
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # "darwin" or "linux"
ARCH=$(uname -m) # "x86_64" or "arm64"
# Normalize architecture name
if [ "$ARCH" = "x86_64" ]; then
ARCH="amd64"
fi
# Get the latest release tag from GitHub
LATEST=$(curl -sI https://github.com/multica-ai/multica/releases/latest | grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n')
# Download and extract
curl -sL "https://github.com/multica-ai/multica/releases/download/${LATEST}/multica_${OS}_${ARCH}.tar.gz" -o /tmp/multica.tar.gz
tar -xzf /tmp/multica.tar.gz -C /tmp multica
sudo mv /tmp/multica /usr/local/bin/multica
rm /tmp/multica.tar.gz
```
Verify:
```bash
multica version
```
**If this fails:**
- Check that `/usr/local/bin` is in `$PATH`.
- On Linux, you may need `chmod +x /usr/local/bin/multica`.
- If `sudo` is not available, install to a user-writable directory: `mv /tmp/multica ~/.local/bin/multica` and ensure `~/.local/bin` is in `$PATH`.
---
## Step 3: Log in
Run:
```bash
multica login
```
**Important:** This command opens a browser window for OAuth authentication. Tell the user:
> "A browser window will open for Multica login. Please complete the authentication in your browser, then come back here."
Wait for the command to complete. It will automatically discover and watch all workspaces the user belongs to.
Verify:
```bash
multica auth status
```
Expected output should show the authenticated user and server URL.
**If login fails:**
- If no browser is available (headless environment), the user can generate a Personal Access Token at `https://app.multica.ai/settings` and run: `multica login --token`
- If the server URL needs to be customized: `multica config set server_url <url>` before logging in.
---
## Step 4: Start the daemon
First, check if the daemon is already running:
```bash
multica daemon status
```
- **If status is "running"**: skip to **Step 5**.
- **If status is "stopped"**: start it:
```bash
multica daemon start
```
Wait 3 seconds, then verify:
```bash
multica daemon status
```
Expected output should show `running` status with detected agents (e.g. `claude`, `codex`).
**If daemon fails to start:**
- Check logs: `multica daemon logs`
- If a port conflict occurs, the daemon may already be running under a different profile.
- If no agents are detected, ensure at least one AI CLI (`claude` or `codex`) is installed and on the `$PATH`.
---
## Step 5: Verify everything is working
Run:
```bash
multica daemon status
```
Confirm:
1. Status is `running`
2. At least one agent is listed (e.g. `claude`, `codex`)
3. At least one workspace is being watched
If the agents list is empty, tell the user:
> "The Multica daemon is running but no AI agent CLIs were detected. Please install at least one: [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`) or [Codex](https://github.com/openai/codex) (`codex`), then restart the daemon with `multica daemon stop && multica daemon start`."
---
## Summary
When all steps are complete, inform the user:
> "Multica CLI is installed and the daemon is running. Agents in your workspaces can now execute tasks on this machine. You can manage workspaces with `multica workspace list` and view daemon logs with `multica daemon logs -f`."

View File

@@ -1,6 +1,6 @@
# Local Development Guide
# Contributing Guide
This guide documents the intended local development workflow for Multica.
This guide documents the local development workflow for contributors working on the Multica codebase.
It covers:
@@ -72,11 +72,11 @@ make worktree-env
That generates values like:
```bash
POSTGRES_DB=multica_super_multica_702
POSTGRES_DB=multica_my_feature_702
POSTGRES_PORT=5432
PORT=18782
FRONTEND_PORT=13702
DATABASE_URL=postgres://multica:multica@localhost:5432/multica_super_multica_702?sslmode=disable
DATABASE_URL=postgres://multica:multica@localhost:5432/multica_my_feature_702?sslmode=disable
```
Notes:
@@ -94,59 +94,52 @@ FORCE=1 make worktree-env
## First-Time Setup
### Main Checkout
### Quick Start (recommended)
From the main checkout:
From any checkout (main or worktree):
```bash
make dev
```
This single command:
- auto-detects whether you're in a main checkout or a worktree
- creates the appropriate env file (`.env` or `.env.worktree`) if it doesn't exist
- checks that prerequisites (Node.js, pnpm, Go, Docker) are installed
- installs JavaScript dependencies
- ensures the shared PostgreSQL container is running
- creates the application database if it does not exist
- runs all migrations
- starts both backend and frontend
### Explicit Setup (advanced)
If you prefer separate control over setup and startup:
#### Main Checkout
```bash
cp .env.example .env
make setup-main
```
What `make setup-main` does:
- installs JavaScript dependencies with `pnpm install`
- ensures the shared PostgreSQL container is running
- creates the application database if it does not exist
- runs all migrations against that database
Start the app:
```bash
make start-main
```
Stop the app processes:
Stop:
```bash
make stop-main
```
This does not stop PostgreSQL.
### Worktree
From the worktree directory:
#### Worktree
```bash
make worktree-env
make setup-worktree
```
What `make setup-worktree` does:
- uses `.env.worktree`
- ensures the shared PostgreSQL container is running
- creates the worktree database if it does not exist
- runs migrations against the worktree database
Start the worktree app:
```bash
make start-worktree
```
Stop the worktree app processes:
Stop:
```bash
make stop-worktree
@@ -169,19 +162,17 @@ make check-main
Use a worktree when you want isolated data and separate app ports.
```bash
git worktree add ../super-multica-feature -b feat/my-change main
cd ../super-multica-feature
make worktree-env
make setup-worktree
make start-worktree
git worktree add ../multica-feature -b feat/my-change main
cd ../multica-feature
make dev
```
After that, day-to-day commands are:
```bash
make start-worktree
make stop-worktree
make check-worktree
make dev # start (re-runs setup if needed, idempotent)
make stop-worktree # stop
make check-worktree # verify
```
## Running Main and Worktree at the Same Time
@@ -195,7 +186,7 @@ Example:
- backend: `8080`
- frontend: `3000`
- worktree checkout
- database: `multica_super_multica_702`
- database: `multica_my_feature_702`
- backend: generated worktree port such as `18782`
- frontend: generated worktree port such as `13702`
@@ -314,18 +305,8 @@ Run the local daemon:
make daemon
```
Normal flow:
1. start the daemon
2. open the pairing link it prints
3. choose the workspace in the browser
4. let the daemon register its local runtime
Debug shortcut:
- you can set `MULTICA_WORKSPACE_ID` in your env file
- this skips normal pairing
- treat it as a local shortcut, not the default workflow
The daemon authenticates using the CLI's stored token (`multica login`).
It registers runtimes for all watched workspaces from the CLI config.
## Troubleshooting
@@ -434,25 +415,21 @@ Warning:
### Stable Main Environment
```bash
cp .env.example .env
make setup-main
make start-main
make dev
```
### Feature Worktree
```bash
git worktree add ../super-multica-feature -b feat/my-change main
cd ../super-multica-feature
make worktree-env
make setup-worktree
make start-worktree
git worktree add ../multica-feature -b feat/my-change main
cd ../multica-feature
make dev
```
### Return to a Previously Configured Worktree
```bash
cd ../super-multica-feature
cd ../multica-feature
make start-worktree
```

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# --- Build stage ---
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
# Cache dependencies
COPY server/go.mod server/go.sum ./server/
RUN cd server && go mod download
# Copy server source
COPY server/ ./server/
# Build binaries
ARG VERSION=dev
ARG COMMIT=unknown
RUN cd server && CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/server ./cmd/server
RUN cd server && CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" -o bin/multica ./cmd/multica
RUN cd server && CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/migrate ./cmd/migrate
# --- Runtime stage ---
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /src/server/bin/server .
COPY --from=builder /src/server/bin/multica .
COPY --from=builder /src/server/bin/migrate .
COPY server/migrations/ ./migrations/
COPY docker/entrypoint.sh .
RUN sed -i 's/\r$//' entrypoint.sh && chmod +x entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

70
Dockerfile.web Normal file
View File

@@ -0,0 +1,70 @@
# --- Dependencies ---
FROM node:22-alpine AS deps
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
WORKDIR /app
# Copy workspace config and all package.json files for dependency resolution
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json .npmrc ./
COPY apps/web/package.json apps/web/
COPY packages/core/package.json packages/core/
COPY packages/ui/package.json packages/ui/
COPY packages/views/package.json packages/views/
COPY packages/tsconfig/package.json packages/tsconfig/
COPY packages/eslint-config/package.json packages/eslint-config/
RUN pnpm install --frozen-lockfile
# --- Build ---
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
WORKDIR /app
# Copy installed dependencies (preserves pnpm symlink structure)
COPY --from=deps /app ./
# Copy source
COPY package.json turbo.json pnpm-workspace.yaml ./
COPY apps/web/ apps/web/
COPY packages/ packages/
# Re-link after source overlay (fixes any symlinks overwritten by COPY)
RUN pnpm install --frozen-lockfile --offline
# Set build-time env: tells Next.js rewrites to proxy API calls to the backend service
ARG REMOTE_API_URL=http://backend:8080
ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV REMOTE_API_URL=$REMOTE_API_URL
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV STANDALONE=true
# Build the web app (standalone output for minimal runtime)
RUN pnpm --filter @multica/web build
# --- Runtime ---
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy standalone output (includes traced node_modules)
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
# Copy static files (not included in standalone)
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
# Copy public assets
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "apps/web/server.js"]

44
LICENSE Normal file
View File

@@ -0,0 +1,44 @@
# Open Source License
Multica is licensed under a modified version of the Apache License 2.0, with the following additional conditions:
1. Multica may be utilized commercially, including as a backend service for
other applications or as a task management platform for enterprises.
Should the conditions below be met, a commercial license must be obtained
from the producer:
a. Hosted or embedded service: Unless explicitly authorized by Multica
in writing, you may not use the Multica source code to provide a
hosted service to third parties, or embed Multica as a component of
a product or service that is sold, licensed, or otherwise
commercially distributed to third parties.
- This restriction applies to offering Multica (in whole or
substantial part) as a SaaS platform, a managed service, or as
an integrated component within another commercial offering.
- Internal use within a single organization (including multiple
workspaces) does not require a commercial license.
b. LOGO and copyright information: In the process of using Multica's
frontend, you may not remove or modify the LOGO or copyright
information in the Multica console or applications. This restriction
is inapplicable to uses of Multica that do not involve its frontend.
- Frontend Definition: For the purposes of this license, the
"frontend" of Multica includes all components located in the
`apps/web/` directory when running Multica from the raw source
code, or the "web" image when running Multica with Docker.
2. As a contributor, you should agree that:
a. The producer can adjust the open-source agreement to be more strict
or relaxed as deemed necessary.
b. Your contributed code may be used for commercial purposes, including
but not limited to its cloud business operations.
Apart from the specific conditions mentioned above, all other rights and
restrictions follow the Apache License 2.0. Detailed information about the
Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
© 2025 Multica, Inc.

View File

@@ -1,4 +1,4 @@
.PHONY: dev daemon cli build test migrate-up migrate-down sqlc seed clean setup start stop check worktree-env setup-main start-main stop-main check-main setup-worktree start-worktree stop-worktree check-worktree db-up db-down
.PHONY: dev server daemon cli multica build test migrate-up migrate-down sqlc seed clean setup start stop check worktree-env setup-main start-main stop-main check-main setup-worktree start-worktree stop-worktree check-worktree db-up db-down selfhost selfhost-stop
MAIN_ENV_FILE ?= .env
WORKTREE_ENV_FILE ?= .env.worktree
@@ -15,6 +15,7 @@ POSTGRES_PORT ?= 5432
PORT ?= 8080
FRONTEND_PORT ?= 3000
FRONTEND_ORIGIN ?= http://localhost:$(FRONTEND_PORT)
MULTICA_APP_URL ?= $(FRONTEND_ORIGIN)
DATABASE_URL ?= postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable
NEXT_PUBLIC_API_URL ?= http://localhost:$(PORT)
NEXT_PUBLIC_WS_URL ?= ws://localhost:$(PORT)/ws
@@ -23,6 +24,8 @@ MULTICA_SERVER_URL ?= ws://localhost:$(PORT)/ws
export
MULTICA_ARGS ?= $(ARGS)
COMPOSE := docker compose
define REQUIRE_ENV
@@ -33,6 +36,53 @@ define REQUIRE_ENV
fi
endef
# ---------- Self-hosting (Docker Compose) ----------
# One-command self-host: create env, start Docker Compose, wait for health
selfhost:
@if [ ! -f .env ]; then \
echo "==> Creating .env from .env.example..."; \
cp .env.example .env; \
JWT=$$(openssl rand -hex 32); \
if [ "$$(uname)" = "Darwin" ]; then \
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=$$JWT/" .env; \
else \
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$$JWT/" .env; \
fi; \
echo "==> Generated random JWT_SECRET"; \
fi
@echo "==> Starting Multica via Docker Compose..."
docker compose -f docker-compose.selfhost.yml up -d --build
@echo "==> Waiting for backend to be ready..."
@for i in $$(seq 1 30); do \
if curl -sf http://localhost:$${PORT:-8080}/health > /dev/null 2>&1; then \
break; \
fi; \
sleep 2; \
done
@if curl -sf http://localhost:$${PORT:-8080}/health > /dev/null 2>&1; then \
echo ""; \
echo "✓ Multica is running!"; \
echo " Frontend: http://localhost:$${FRONTEND_PORT:-3000}"; \
echo " Backend: http://localhost:$${PORT:-8080}"; \
echo ""; \
echo "Log in with any email + verification code: 888888"; \
echo ""; \
echo "Next — install the CLI and connect your machine:"; \
echo " brew install multica-ai/tap/multica"; \
echo " multica setup --local"; \
else \
echo ""; \
echo "Services are still starting. Check logs:"; \
echo " docker compose -f docker-compose.selfhost.yml logs"; \
fi
# Stop all Docker Compose self-host services
selfhost-stop:
@echo "==> Stopping Multica services..."
docker compose -f docker-compose.selfhost.yml down
@echo "✓ All services stopped."
# ---------- One-click commands ----------
# First-time setup: install deps, start DB, run migrations
@@ -66,7 +116,12 @@ stop:
@echo "Stopping services..."
@-lsof -ti:$(PORT) | xargs kill -9 2>/dev/null
@-lsof -ti:$(FRONTEND_PORT) | xargs kill -9 2>/dev/null
@echo "✓ App processes stopped. Shared PostgreSQL is still running on localhost:5432."
@case "$(DATABASE_URL)" in \
""|*@localhost:*|*@localhost/*|*@127.0.0.1:*|*@127.0.0.1/*|*@\[::1\]:*|*@\[::1\]/*) \
echo "✓ App processes stopped. Shared PostgreSQL is still running on localhost:$(POSTGRES_PORT)." ;; \
*) \
echo "✓ App processes stopped. Remote PostgreSQL was not affected." ;; \
esac
# Full verification: typecheck + unit tests + Go tests + E2E
check:
@@ -95,6 +150,12 @@ check-main:
@ENV_FILE=$(MAIN_ENV_FILE) bash scripts/check.sh
setup-worktree:
@if [ ! -f "$(WORKTREE_ENV_FILE)" ]; then \
echo "==> Generating $(WORKTREE_ENV_FILE) with unique ports..."; \
bash scripts/init-worktree-env.sh $(WORKTREE_ENV_FILE); \
else \
echo "==> Using existing $(WORKTREE_ENV_FILE)"; \
fi
@$(MAKE) setup ENV_FILE=$(WORKTREE_ENV_FILE)
start-worktree:
@@ -108,28 +169,38 @@ check-worktree:
# ---------- Individual commands ----------
# Go server
# One-command dev: auto-setup env/deps/db/migrations, then start all services
dev:
@bash scripts/dev.sh
# Go server only
server:
$(REQUIRE_ENV)
@bash scripts/ensure-postgres.sh "$(ENV_FILE)"
cd server && go run ./cmd/server
daemon:
cd server && MULTICA_REPOS_ROOT="${MULTICA_REPOS_ROOT:-$(abspath .)}" go run ./cmd/multica daemon
@$(MAKE) multica MULTICA_ARGS="daemon"
cli:
cd server && go run ./cmd/multica $(ARGS)
@$(MAKE) multica MULTICA_ARGS="$(MULTICA_ARGS)"
multica:
cd server && go run ./cmd/multica $(MULTICA_ARGS)
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
build:
cd server && go build -o bin/server ./cmd/server
cd server && go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o bin/multica ./cmd/multica
cd server && go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" -o bin/multica ./cmd/multica
cd server && go build -o bin/migrate ./cmd/migrate
test:
$(REQUIRE_ENV)
@bash scripts/ensure-postgres.sh "$(ENV_FILE)"
cd server && go run ./cmd/migrate up
cd server && go test ./...
# Database

229
README.md
View File

@@ -1,142 +1,159 @@
<p align="center">
<img src="docs/assets/banner.jpg" alt="Multica — humans and agents, side by side" width="100%">
</p>
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-light.svg">
<img alt="Multica" src="docs/assets/logo-light.svg" width="50">
</picture>
# Multica
AI-native task management platform — like Linear, but with AI agents as first-class citizens.
**Your next 10 hires won't be human.**
For the full local development workflow, see [Local Development Guide](LOCAL_DEVELOPMENT.md).
The open-source managed agents platform.<br/>
Turn coding agents into real teammates — assign tasks, track progress, compound skills.
## Prerequisites
[![CI](https://github.com/multica-ai/multica/actions/workflows/ci.yml/badge.svg)](https://github.com/multica-ai/multica/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GitHub stars](https://img.shields.io/github/stars/multica-ai/multica?style=flat)](https://github.com/multica-ai/multica/stargazers)
- [Node.js](https://nodejs.org/) (v20+)
- [pnpm](https://pnpm.io/) (v10.28+)
- [Go](https://go.dev/) (v1.26+)
- [Docker](https://www.docker.com/)
[Website](https://multica.ai) · [Cloud](https://multica.ai/app) · [X](https://x.com/multica_hq) · [Self-Hosting](SELF_HOSTING.md) · [Contributing](CONTRIBUTING.md)
## Quick Start
**English | [简体中文](README.zh-CN.md)**
</div>
## What is Multica?
Multica turns coding agents into real teammates. Assign issues to an agent like you'd assign to a colleague — they'll pick up the work, write code, report blockers, and update statuses autonomously.
No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **OpenClaw**, and **OpenCode**.
<p align="center">
<img src="docs/assets/hero-screenshot.png" alt="Multica board view" width="800">
</p>
## Features
Multica manages the full agent lifecycle: from task assignment to execution monitoring to skill reuse.
- **Agents as Teammates** — assign to an agent like you'd assign to a colleague. They have profiles, show up on the board, post comments, create issues, and report blockers proactively.
- **Autonomous Execution** — set it and forget it. Full task lifecycle management (enqueue, claim, start, complete/fail) with real-time progress streaming via WebSocket.
- **Reusable Skills** — every solution becomes a reusable skill for the whole team. Deployments, migrations, code reviews — skills compound your team's capabilities over time.
- **Unified Runtimes** — one dashboard for all your compute. Local daemons and cloud runtimes, auto-detection of available CLIs, real-time monitoring.
- **Multi-Workspace** — organize work across teams with workspace-level isolation. Each workspace has its own agents, issues, and settings.
---
## Quick Install
```bash
# 1. Install dependencies
pnpm install
# 2. Copy environment variables for the shared main environment
cp .env.example .env
# 3. One-time setup: ensure shared PostgreSQL, create the app DB, run migrations
make setup
# 4. Start backend + frontend
make start
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
Open your configured `FRONTEND_ORIGIN` in the browser. By default that is [http://localhost:3000](http://localhost:3000).
Installs the Multica CLI on macOS and Linux. Works with Homebrew or downloads the binary directly.
Main checkout uses `.env`. A Git worktree should generate its own `.env.worktree` and use the explicit worktree targets:
After installation:
```bash
make worktree-env
make setup-worktree
make start-worktree
multica login # Authenticate (opens browser)
multica daemon start # Start the local agent runtime
multica daemon stop # Stop the daemon when done
```
Every checkout shares the same PostgreSQL container on `localhost:5432`. Isolation now happens at the database level:
> **Self-hosting?** Add `--local` to deploy a full Multica server on your machine:
>
> ```bash
> curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
> ```
>
> Requires Docker. See the [Self-Hosting Guide](SELF_HOSTING.md) for details.
- `.env` typically uses `POSTGRES_DB=multica`
- each `.env.worktree` gets its own `POSTGRES_DB`, such as `multica_super_multica_702`
- backend/frontend ports still stay unique per worktree
---
That keeps one Docker container and one volume, while still isolating schema and data per worktree.
## Getting Started
## Project Structure
### 1. Log in and start the daemon
```
├── server/ # Go backend (Chi + sqlc + gorilla/websocket)
│ ├── cmd/ # server, daemon, migrate
│ ├── internal/ # Core business logic
│ ├── migrations/ # SQL migrations
│ └── sqlc.yaml # sqlc config
├── apps/
│ └── web/ # Next.js 16 frontend
├── packages/ # Shared TypeScript packages
│ ├── ui/ # Component library (shadcn/ui + Radix)
│ ├── types/ # Shared type definitions
│ ├── sdk/ # API client SDK
│ ├── store/ # State management
│ ├── hooks/ # Shared React hooks
│ └── utils/ # Utility functions
├── Makefile # Backend commands
├── docker-compose.yml # PostgreSQL + pgvector
└── .env.example # Environment variable template
```bash
multica login # Authenticate with your Multica account
multica daemon start # Start the local agent runtime
```
## Commands
The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) on your PATH.
### Frontend
### 2. Verify your runtime
Open your workspace in the Multica web app. Navigate to **Settings → Runtimes** — you should see your machine listed as an active **Runtime**.
> **What is a Runtime?** A Runtime is a compute environment that can execute agent tasks. It can be your local machine (via the daemon) or a cloud instance. Each runtime reports which agent CLIs are available, so Multica knows where to route work.
### 3. Create an agent
Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, or OpenCode). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
### 4. Assign your first task
Create an issue from the board (or via `multica issue create`), then assign it to your new agent. The agent will automatically pick up the task, execute it on your runtime, and report progress — just like a human teammate.
---
## CLI
The `multica` CLI connects your local machine to Multica — authenticate, manage workspaces, and run the agent daemon.
| Command | Description |
|---------|-------------|
| `pnpm dev:web` | Start Next.js dev server (uses `FRONTEND_PORT`, default `3000`) |
| `pnpm build` | Build all TypeScript packages |
| `pnpm typecheck` | Run TypeScript type checking |
| `pnpm test` | Run TypeScript tests |
| `multica login` | Authenticate (opens browser) |
| `multica daemon start` | Start the local agent runtime |
| `multica daemon status` | Check daemon status |
| `multica setup` | One-command setup (configure + login + start daemon) |
| `multica setup --local` | Same, but for self-hosted deployments |
| `multica config local` | Configure CLI for a local self-hosted server |
| `multica issue list` | List issues in your workspace |
| `multica issue create` | Create a new issue |
| `multica update` | Update to the latest version |
### Backend
See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference.
| Command | Description |
|---------|-------------|
| `make dev` | Run Go server (uses `PORT`, default `8080`) |
| `make daemon` | Run local agent daemon |
| `make test` | Run Go tests |
| `make build` | Build server & daemon binaries |
| `make sqlc` | Regenerate sqlc code from SQL |
---
### Database
## Architecture
| Command | Description |
|---------|-------------|
| `make db-up` | Start the shared PostgreSQL container |
| `make db-down` | Stop the shared PostgreSQL container |
| `make migrate-up` | Ensure the current DB exists, then run migrations |
| `make migrate-down` | Rollback database migrations for the current DB |
| `make worktree-env` | Generate an isolated `.env.worktree` for the current worktree |
| `make setup-main` / `make start-main` | Force use of the shared main `.env` |
| `make setup-worktree` / `make start-worktree` | Force use of isolated `.env.worktree` |
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go Backend │────>│ PostgreSQL │
│ Frontend │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (runs on your machine)
│Claude/Codex/ │
│OpenClaw/Code │
└──────────────┘
```
## Environment Variables
| Layer | Stack |
|-------|-------|
| Frontend | Next.js 16 (App Router) |
| Backend | Go (Chi router, sqlc, gorilla/websocket) |
| Database | PostgreSQL 17 with pgvector |
| Agent Runtime | Local daemon executing Claude Code, Codex, OpenClaw, or OpenCode |
See [`.env.example`](.env.example) for all available variables:
## Development
- `DATABASE_URL` — PostgreSQL connection string
- `POSTGRES_DB` — Database name for the current checkout or worktree
- `POSTGRES_PORT` — Shared PostgreSQL host port (fixed to `5432`)
- `PORT` — Backend server port (default: 8080)
- `FRONTEND_PORT` / `FRONTEND_ORIGIN` — Frontend port and browser origin
- `JWT_SECRET` — JWT signing secret
- `MULTICA_APP_URL` — Browser origin used when generating local runtime pairing links
- `MULTICA_DAEMON_CONFIG` — Optional path for the daemon's persisted local config
- `MULTICA_WORKSPACE_ID` — Optional dev override for the workspace id; normal usage should rely on browser pairing instead
- `MULTICA_DAEMON_ID` / `MULTICA_DAEMON_DEVICE_NAME` — Stable daemon identity for local runtime registration
- `MULTICA_CODEX_PATH` / `MULTICA_CODEX_MODEL` — Codex executable and optional model override for local task execution
- `MULTICA_CODEX_WORKDIR` — Default working directory used by the local Codex runtime
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` — Google OAuth (optional)
- `NEXT_PUBLIC_API_URL` — Frontend → backend API URL
- `NEXT_PUBLIC_WS_URL` — Frontend → backend WebSocket URL
For contributors working on the Multica codebase, see the [Contributing Guide](CONTRIBUTING.md).
## Local Codex Daemon
**Prerequisites:** [Node.js](https://nodejs.org/) v20+, [pnpm](https://pnpm.io/) v10.28+, [Go](https://go.dev/) v1.26+, [Docker](https://www.docker.com/)
The local daemon currently supports one local runtime type: `codex`.
```bash
make dev
```
1. Start the daemon with `make daemon`.
2. If the daemon does not already know its workspace, it prints a pairing link in the terminal.
3. Open that link in the browser, sign in, and choose the workspace that should own the local Codex runtime.
4. The daemon stores the approved workspace locally in `MULTICA_DAEMON_CONFIG` or `~/.multica/daemon.json`.
5. The daemon registers the local Codex runtime via `/api/daemon/register`.
6. Create an agent in Multica and bind it to that runtime.
7. Assign an issue to the agent and move the issue to `todo`.
8. The daemon claims the task, runs `codex exec`, and reports the final comment back to the issue.
`make dev` auto-detects your environment (main checkout or worktree), creates the env file, installs dependencies, sets up the database, runs migrations, and starts all services.
For local development you can still set `MULTICA_WORKSPACE_ID` directly to skip pairing, but that should be treated as a debug shortcut rather than the normal flow.
## Local Development Notes
- `make setup`, `make start`, `make dev`, and `make test` now require an env file. They fail fast if `.env` or `.env.worktree` is missing.
- `make stop` only stops the backend/frontend processes for the current checkout. It does not stop the shared PostgreSQL container.
- Use `make db-down` only when you explicitly want to shut down the shared local PostgreSQL instance for every checkout.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow, worktree support, testing, and troubleshooting.

146
README.zh-CN.md Normal file
View File

@@ -0,0 +1,146 @@
<p align="center">
<img src="docs/assets/banner.jpg" alt="Multica — 人类与 AI并肩前行" width="100%">
</p>
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="docs/assets/logo-light.svg">
<img alt="Multica" src="docs/assets/logo-light.svg" width="50">
</picture>
# Multica
**你的下一批员工,不是人类。**
开源的 Managed Agents 平台。<br/>
将编码 Agent 变成真正的队友——分配任务、跟踪进度、积累技能。
[![CI](https://github.com/multica-ai/multica/actions/workflows/ci.yml/badge.svg)](https://github.com/multica-ai/multica/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GitHub stars](https://img.shields.io/github/stars/multica-ai/multica?style=flat)](https://github.com/multica-ai/multica/stargazers)
[官网](https://multica.ai) · [云服务](https://multica.ai/app) · [X](https://x.com/multica_hq) · [自部署指南](SELF_HOSTING.md) · [参与贡献](CONTRIBUTING.md)
**[English](README.md) | 简体中文**
</div>
## Multica 是什么?
Multica 将编码 Agent 变成真正的队友。像分配给同事一样分配给 Agent——它们会自主接手工作、编写代码、报告阻塞问题、更新状态。
不再需要复制粘贴 prompt不再需要盯着运行过程。你的 Agent 出现在看板上、参与对话、随着时间积累可复用的技能。可以理解为开源的 Managed Agents 基础设施——厂商中立、可自部署、专为人类 + AI 团队设计。支持 **Claude Code**、**Codex**、**OpenClaw** 和 **OpenCode**
<p align="center">
<img src="docs/assets/hero-screenshot.png" alt="Multica 看板视图" width="800">
</p>
## 功能特性
Multica 管理完整的 Agent 生命周期:从任务分配到执行监控再到技能复用。
- **Agent 即队友** — 像分配给同事一样分配给 Agent。它们有个人档案、出现在看板上、发表评论、创建 Issue、主动报告阻塞问题。
- **自主执行** — 设置后无需管理。完整的任务生命周期管理(排队、认领、执行、完成/失败),通过 WebSocket 实时推送进度。
- **可复用技能** — 每个解决方案都成为全团队可复用的技能。部署、数据库迁移、代码审查——技能让团队能力随时间持续增长。
- **统一运行时** — 一个控制台管理所有算力。本地 daemon 和云端运行时,自动检测可用 CLI实时监控。
- **多工作区** — 按团队组织工作,工作区级别隔离。每个工作区有独立的 Agent、Issue 和设置。
---
## 快速安装
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
安装 Multica CLI支持 macOS 和 Linux。有 Homebrew 用 Homebrew没有则直接下载二进制。
安装完成后:
```bash
multica login # 认证(打开浏览器)
multica daemon start # 启动本地 Agent 运行时
multica daemon stop # 停止 daemon
```
> **自部署?** 加上 `--local` 在本地部署完整的 Multica 服务:
>
> ```bash
> curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
> ```
>
> 需要 Docker。详见 [自部署指南](SELF_HOSTING.md)。
---
## 快速上手
安装好 CLI或注册 [Multica 云服务](https://multica.ai))后,按以下步骤将第一个任务分配给 Agent
### 1. 登录并启动 daemon
```bash
multica login # 使用你的 Multica 账号认证
multica daemon start # 启动本地 Agent 运行时
```
daemon 在后台运行,保持你的机器与 Multica 的连接。它会自动检测 PATH 中可用的 Agent CLI`claude``codex``openclaw``opencode`)。
### 2. 确认运行时已连接
在 Multica Web 端打开你的工作区,进入 **设置 → 运行时Runtimes**,你应该能看到你的机器已作为一个活跃的 **Runtime** 出现在列表中。
> **什么是 Runtime运行时** Runtime 是可以执行 Agent 任务的计算环境。它可以是你的本地机器(通过 daemon 连接),也可以是云端实例。每个 Runtime 会上报可用的 Agent CLIMultica 据此决定将任务路由到哪里执行。
### 3. 创建 Agent
进入 **设置 → Agents**,点击 **新建 Agent**。选择你刚连接的 Runtime选择 ProviderClaude Code、Codex、OpenClaw 或 OpenCode并为 Agent 起个名字——它将以这个名字出现在看板、评论和任务分配中。
### 4. 分配你的第一个任务
在看板上创建一个 Issue或通过 `multica issue create` 命令创建),然后将其分配给你的新 Agent。Agent 会自动接手任务、在你的 Runtime 上执行、并实时汇报进度——就像一个真正的队友一样。
大功告成!你的 Agent 现在是团队的一员了。 🎉
## 架构
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go 后端 │────>│ PostgreSQL │
│ 前端 │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (运行在你的机器上)
│Claude/Codex/ │
│OpenClaw/Code │
└──────────────┘
```
| 层级 | 技术栈 |
|------|--------|
| 前端 | Next.js 16 (App Router) |
| 后端 | Go (Chi router, sqlc, gorilla/websocket) |
| 数据库 | PostgreSQL 17 with pgvector |
| Agent 运行时 | 本地 daemon 执行 Claude Code、Codex、OpenClaw 或 OpenCode |
## 开发
参与 Multica 代码贡献,请参阅 [贡献指南](CONTRIBUTING.md)。
**环境要求:** [Node.js](https://nodejs.org/) v20+, [pnpm](https://pnpm.io/) v10.28+, [Go](https://go.dev/) v1.26+, [Docker](https://www.docker.com/)
```bash
pnpm install
cp .env.example .env
make setup
make start
```
完整的开发流程、worktree 支持、测试和问题排查请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 开源协议
[Apache 2.0](LICENSE)

207
SELF_HOSTING.md Normal file
View File

@@ -0,0 +1,207 @@
# Self-Hosting Guide
Deploy Multica on your own infrastructure in minutes.
## Architecture
| Component | Description | Technology |
|-----------|-------------|------------|
| **Backend** | REST API + WebSocket server | Go (single binary) |
| **Frontend** | Web application | Next.js 16 |
| **Database** | Primary data store | PostgreSQL 17 with pgvector |
Each user who runs AI agents locally also installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
## Quick Install (Recommended)
One command to set up everything — server, CLI, and configuration:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
```
This automatically clones the repository, starts all services via Docker Compose, and installs the `multica` CLI.
Once complete, open http://localhost:3000, log in with any email + verification code **`888888`**, then:
```bash
multica login # Authenticate (opens browser)
multica daemon start # Start the agent daemon
```
> **Prerequisites:** Docker and Docker Compose must be installed. The script checks for this and provides install links if missing.
---
## Step-by-Step Setup (Alternative)
If you prefer to run each step manually:
### Step 1 — Start the Server
**Prerequisites:** Docker and Docker Compose.
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
```
`make selfhost` automatically creates `.env` from the example, generates a random `JWT_SECRET`, and starts all services via Docker Compose.
Once ready:
- **Frontend:** http://localhost:3000
- **Backend API:** http://localhost:8080
> **Note:** If you prefer to run the Docker Compose steps manually, see [Manual Docker Compose Setup](#manual-docker-compose-setup) below.
### Step 2 — Log In
Open http://localhost:3000 in your browser. Enter any email address and use verification code **`888888`** to log in.
> This master code works in all non-production environments (i.e. when `APP_ENV` is not set to `production`). For production, configure an email provider — see [Advanced Configuration](SELF_HOSTING_ADVANCED.md#email-required-for-authentication).
### Step 3 — Install CLI & Start Daemon
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them with the server, and executes tasks when agents are assigned work.
Each team member who wants to run AI agents locally needs to:
### a) Install the CLI and an AI agent
```bash
brew install multica-ai/tap/multica
```
You also need at least one AI agent CLI installed:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
### b) One-command setup
```bash
multica setup --local
```
This automatically:
1. Configures the CLI to connect to `localhost` (ports 8080/3000)
2. Opens your browser for authentication
3. Discovers your workspaces
4. Starts the daemon in the background
To verify the daemon is running:
```bash
multica daemon status
```
> **Alternative:** If you prefer manual steps, see [Manual CLI Configuration](#manual-cli-configuration) below.
### Step 4 — Verify & Start Using
1. Open your workspace in the web app at http://localhost:3000
2. Navigate to **Settings → Runtimes** — you should see your machine listed
3. Go to **Settings → Agents** and create a new agent
4. Create an issue and assign it to your agent — it will pick up the task automatically
## Stopping Services
If you installed via the install script:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --stop
```
If you cloned the repo manually:
```bash
# Stop the Docker Compose services (backend, frontend, database)
make selfhost-stop
# Stop the local daemon
multica daemon stop
```
## Switching to Multica Cloud
If you've been self-hosting and want to switch your CLI to [Multica Cloud](https://multica.ai):
```bash
multica config set server_url https://api.multica.ai
multica config set app_url https://multica.ai
multica login
```
Or re-run the install script without `--local` — it will reconfigure the CLI automatically:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
> Your local Docker services are unaffected. Stop them separately if you no longer need them.
## Rebuilding After Updates
```bash
git pull
make selfhost
```
Migrations run automatically on backend startup.
---
## Manual Docker Compose Setup
If you prefer running Docker Compose steps manually instead of `make selfhost`:
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
cp .env.example .env
```
Edit `.env` — at minimum, change `JWT_SECRET`:
```bash
JWT_SECRET=$(openssl rand -hex 32)
```
Then start everything:
```bash
docker compose -f docker-compose.selfhost.yml up -d
```
## Manual CLI Configuration
If you prefer configuring the CLI step by step instead of `multica setup`:
```bash
# Point CLI to your local server
multica config local
# Or set URLs manually:
# multica config set app_url http://localhost:3000
# multica config set server_url http://localhost:8080
# Login (opens browser)
multica login
# Start the daemon
multica daemon start
```
For production deployments with TLS:
```bash
multica config set app_url https://app.example.com
multica config set server_url https://api.example.com
multica login
multica daemon start
```
## Advanced Configuration
For environment variables, manual setup (without Docker), reverse proxy configuration, database setup, and more, see the [Advanced Configuration Guide](SELF_HOSTING_ADVANCED.md).

224
SELF_HOSTING_ADVANCED.md Normal file
View File

@@ -0,0 +1,224 @@
# Self-Hosting — Advanced Configuration
This document covers advanced configuration for self-hosted Multica deployments. For the quick start guide, see [SELF_HOSTING.md](SELF_HOSTING.md).
## Configuration
All configuration is done via environment variables. Copy `.env.example` as a starting point.
### Required Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | PostgreSQL connection string | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` |
| `JWT_SECRET` | **Must change from default.** Secret key for signing JWT tokens. Use a long random string. | `openssl rand -hex 32` |
| `FRONTEND_ORIGIN` | URL where the frontend is served (used for CORS) | `https://app.example.com` |
### Email (Required for Authentication)
Multica uses email-based magic link authentication via [Resend](https://resend.com).
| Variable | Description |
|----------|-------------|
| `RESEND_API_KEY` | Your Resend API key |
| `RESEND_FROM_EMAIL` | Sender email address (default: `noreply@multica.ai`) |
> **Note:** For local/development deployments without email configured, you can use the master verification code `888888` to log in.
### Google OAuth (Optional)
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
| `GOOGLE_REDIRECT_URI` | OAuth callback URL (e.g. `https://app.example.com/auth/callback`) |
### File Storage (Optional)
For file uploads and attachments, configure S3 and CloudFront:
| Variable | Description |
|----------|-------------|
| `S3_BUCKET` | S3 bucket name |
| `S3_REGION` | AWS region (default: `us-west-2`) |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
| `CLOUDFRONT_PRIVATE_KEY` | CloudFront private key (PEM format) |
| `COOKIE_DOMAIN` | Domain for CloudFront auth cookies |
### Server
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `8080` | Backend server port |
| `FRONTEND_PORT` | `3000` | Frontend port |
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins |
| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
### CLI / Daemon
These are configured on each user's machine, not on the server:
| Variable | Default | Description |
|----------|---------|-------------|
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | WebSocket URL for daemon → server connection |
| `MULTICA_APP_URL` | `http://localhost:3000` | Frontend URL for CLI login flow |
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | How often the daemon polls for tasks |
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | Heartbeat frequency |
## Database Setup
Multica requires PostgreSQL 17 with the pgvector extension.
### Using Docker Compose (Recommended)
The `docker-compose.selfhost.yml` includes PostgreSQL. No separate setup needed.
### Using Your Own PostgreSQL
If you prefer to use an existing PostgreSQL instance, ensure the pgvector extension is available:
```sql
CREATE EXTENSION IF NOT EXISTS vector;
```
Set `DATABASE_URL` in your `.env` and remove the `postgres` service from the compose file.
### Running Migrations Manually
The Docker Compose setup runs migrations automatically. If you need to run them manually:
```bash
# Using the built binary
./server/bin/migrate up
# Or from source
cd server && go run ./cmd/migrate up
```
## Manual Setup (Without Docker Compose)
If you prefer to build and run services manually:
**Prerequisites:** Go 1.26+, Node.js 20+, pnpm 10.28+, PostgreSQL 17 with pgvector.
```bash
# Start your PostgreSQL (or use: docker compose up -d postgres)
# Build the backend
make build
# Run database migrations
DATABASE_URL="your-database-url" ./server/bin/migrate up
# Start the backend server
DATABASE_URL="your-database-url" PORT=8080 JWT_SECRET="your-secret" ./server/bin/server
```
For the frontend:
```bash
pnpm install
pnpm build
# Start the frontend (production mode)
cd apps/web
REMOTE_API_URL=http://localhost:8080 pnpm start
```
## Reverse Proxy
In production, put a reverse proxy in front of both the backend and frontend to handle TLS and routing.
### Caddy (Recommended)
```
app.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8080
}
```
### Nginx
```nginx
# Frontend
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Backend API
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket support
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}
```
When using separate domains for frontend and backend, set these environment variables accordingly:
```bash
# Backend
FRONTEND_ORIGIN=https://app.example.com
CORS_ALLOWED_ORIGINS=https://app.example.com
# Frontend (set before building the frontend image)
REMOTE_API_URL=https://api.example.com
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_WS_URL=wss://api.example.com/ws
```
## Health Check
The backend exposes a health check endpoint:
```
GET /health
→ {"status":"ok"}
```
Use this for load balancer health checks or monitoring.
## Upgrading
```bash
git pull
docker compose -f docker-compose.selfhost.yml up -d --build
```
Migrations run automatically on backend startup. They are idempotent — running them multiple times has no effect.

74
SELF_HOSTING_AI.md Normal file
View File

@@ -0,0 +1,74 @@
# Self-Hosting Setup (for AI Agents)
This document is designed for AI agents to execute. Follow these steps exactly to deploy a local Multica instance and connect to it.
## Prerequisites
- Docker and Docker Compose installed
- Homebrew installed (for CLI)
- At least one AI agent CLI on PATH: `claude` or `codex`
## One-Command Install
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
```
This installs everything: clones the repo, starts Docker services, and installs the CLI.
Wait for the output `✓ Multica is installed and running!` before proceeding.
**Expected result:**
- Frontend at http://localhost:3000
- Backend at http://localhost:8080
- `multica` CLI installed and configured for localhost
## Alternative: Manual Setup
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
brew install multica-ai/tap/multica
multica setup --local
```
The `multica setup --local` command will:
1. Configure CLI to connect to localhost:8080 / localhost:3000
2. Open a browser for login — use verification code `888888` with any email
3. Discover workspaces automatically
4. Start the daemon in the background
## Verification
```bash
multica daemon status
```
Should show `running` with detected agents.
## Stopping
```bash
# Stop the daemon
multica daemon stop
# Stop all Docker services
cd multica
make selfhost-stop
```
## Custom Ports
If the default ports (8080/3000) are in use:
1. Edit `.env` and change `PORT` and `FRONTEND_PORT`
2. Run `make selfhost`
3. Run `multica setup --local --port <PORT> --frontend-port <FRONTEND_PORT>`
## Troubleshooting
- **Backend not ready:** `docker compose -f docker-compose.selfhost.yml logs backend`
- **Frontend not ready:** `docker compose -f docker-compose.selfhost.yml logs frontend`
- **Daemon issues:** `multica daemon logs`
- **Health check:** `curl http://localhost:8080/health`

6
apps/desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
out
.DS_Store
.eslintcache
*.log*

View File

@@ -0,0 +1,31 @@
appId: ai.multica.desktop
productName: Multica
directories:
buildResources: build
files:
- "!**/.vscode/*"
- "!src/*"
- "!electron.vite.config.*"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
asarUnpack:
- resources/**
mac:
entitlementsInherit: build/entitlements.mac.plist
target:
- dmg
- zip
artifactName: ${name}-${version}-${arch}.${ext}
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- deb
artifactName: ${name}-${version}-${arch}.${ext}
win:
target:
- nsis
artifactName: ${name}-${version}-setup.${ext}
npmRebuild: false

View File

@@ -0,0 +1,26 @@
import { resolve } from "path";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
server: {
port: 5173,
strictPort: true,
},
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": resolve("src/renderer/src"),
},
dedupe: ["react", "react-dom"],
},
},
});

View File

@@ -0,0 +1,6 @@
import reactConfig from "@multica/eslint-config/react";
export default [
...reactConfig,
{ ignores: ["out/", "dist/"] },
];

50
apps/desktop/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "@multica/desktop",
"version": "0.1.0",
"private": true,
"main": "./out/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"preview": "electron-vite preview",
"package": "electron-builder",
"lint": "eslint .",
"postinstall": "electron-builder install-app-deps"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@multica/core": "workspace:*",
"@multica/ui": "workspace:*",
"@multica/views": "workspace:*",
"@fontsource/geist-mono": "^5.2.7",
"@fontsource/geist-sans": "^5.2.5",
"react-router-dom": "^7.6.0",
"shadcn": "^4.1.0",
"sonner": "^2.0.7",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@electron-toolkit/tsconfig": "^2.0.0",
"@multica/tsconfig": "workspace:*",
"@tailwindcss/vite": "^4",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "^5.1.1",
"electron": "^39.2.6",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"react": "catalog:",
"react-dom": "catalog:",
"tailwindcss": "^4",
"typescript": "catalog:"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,56 @@
import { app, shell, BrowserWindow } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
let mainWindow: BrowserWindow | null = null;
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 900,
minHeight: 600,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 13 },
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
webSecurity: false,
},
});
mainWindow.on("ready-to-show", () => {
mainWindow?.show();
});
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: "deny" };
});
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
} else {
mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
}
}
app.whenReady().then(() => {
electronApp.setAppUserModelId("ai.multica.desktop");
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

9
apps/desktop/src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { ElectronAPI } from "@electron-toolkit/preload";
declare global {
interface Window {
electron: ElectronAPI;
}
}
export {};

View File

@@ -0,0 +1,9 @@
import { contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
if (process.contextIsolated) {
contextBridge.exposeInMainWorld("electron", electronAPI);
} else {
// @ts-expect-error - fallback for non-isolated context
window.electron = electronAPI;
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multica</title>
</head>
<body class="h-full overflow-hidden antialiased font-sans">
<div id="root" class="h-full"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
import { CoreProvider } from "@multica/core/platform";
import { useAuthStore } from "@multica/core/auth";
import { ThemeProvider } from "@multica/ui/components/common/theme-provider";
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
import { Toaster } from "sonner";
import { DesktopLoginPage } from "./pages/login";
import { DesktopShell } from "./components/desktop-layout";
function AppContent() {
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6 animate-pulse" />
</div>
);
}
if (!user) return <DesktopLoginPage />;
return <DesktopShell />;
}
export default function App() {
return (
<ThemeProvider>
<CoreProvider
apiBaseUrl={import.meta.env.VITE_API_URL || "http://localhost:8080"}
wsUrl={import.meta.env.VITE_WS_URL || "ws://localhost:8080/ws"}
>
<AppContent />
</CoreProvider>
<Toaster />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,102 @@
import { useEffect } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useTabHistory } from "@/hooks/use-tab-history";
import { useActiveTitleSync } from "@/hooks/use-tab-sync";
import { useTabStore, resolveRouteIcon } from "@/stores/tab-store";
import { SidebarProvider } from "@multica/ui/components/ui/sidebar";
import { ModalRegistry } from "@multica/views/modals/registry";
import { AppSidebar, DashboardGuard } from "@multica/views/layout";
import { SearchCommand, SearchTrigger } from "@multica/views/search";
import { ChatFab, ChatWindow } from "@multica/views/chat";
import { DesktopNavigationProvider } from "@/platform/navigation";
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
import { TabBar } from "./tab-bar";
import { TabContent } from "./tab-content";
function SidebarTopBar() {
const { canGoBack, canGoForward, goBack, goForward } = useTabHistory();
return (
<div
className="h-12 shrink-0 flex items-center justify-end px-2"
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
>
<div
className="flex items-center gap-0.5"
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
<button
onClick={goBack}
disabled={!canGoBack}
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-30 disabled:pointer-events-none"
>
<ChevronLeft className="size-4" />
</button>
<button
onClick={goForward}
disabled={!canGoForward}
className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-30 disabled:pointer-events-none"
>
<ChevronRight className="size-4" />
</button>
</div>
</div>
);
}
function useInternalLinkHandler() {
useEffect(() => {
const handler = (e: Event) => {
const path = (e as CustomEvent).detail?.path;
if (!path) return;
const icon = resolveRouteIcon(path);
const store = useTabStore.getState();
const tabId = store.openTab(path, path, icon);
store.setActiveTab(tabId);
};
window.addEventListener("multica:navigate", handler);
return () => window.removeEventListener("multica:navigate", handler);
}, []);
}
export function DesktopShell() {
useInternalLinkHandler();
useActiveTitleSync();
return (
<DesktopNavigationProvider>
<DashboardGuard
loginPath="/login"
loadingFallback={
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6 animate-pulse" />
</div>
}
>
<div className="flex h-screen">
<SidebarProvider className="flex-1">
<AppSidebar topSlot={<SidebarTopBar />} searchSlot={<SearchTrigger />} />
{/* Right side: header + content container */}
<div className="flex flex-1 min-w-0 flex-col">
{/* Tab bar + drag region */}
<header
className="h-12 shrink-0"
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
>
<TabBar />
</header>
{/* Content area with inset styling */}
<div className="flex flex-1 min-h-0 flex-col overflow-hidden mr-2 mb-2 ml-0.5 rounded-xl shadow-sm bg-background">
<TabContent />
</div>
</div>
</SidebarProvider>
</div>
<ModalRegistry />
<SearchCommand />
<ChatWindow />
<ChatFab />
</DashboardGuard>
</DesktopNavigationProvider>
);
}

View File

@@ -0,0 +1,188 @@
import {
Inbox,
CircleUser,
ListTodo,
Bot,
Monitor,
BookOpenText,
Settings,
X,
Plus,
type LucideIcon,
} from "lucide-react";
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
closestCenter,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
horizontalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import {
restrictToHorizontalAxis,
restrictToParentElement,
} from "@dnd-kit/modifiers";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@multica/ui/lib/utils";
import { useTabStore, resolveRouteIcon, type Tab } from "@/stores/tab-store";
const TAB_ICONS: Record<string, LucideIcon> = {
Inbox,
CircleUser,
ListTodo,
Bot,
Monitor,
BookOpenText,
Settings,
};
function SortableTabItem({ tab, isActive, isOnly }: { tab: Tab; isActive: boolean; isOnly: boolean }) {
const setActiveTab = useTabStore((s) => s.setActiveTab);
const closeTab = useTabStore((s) => s.closeTab);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: tab.id });
const Icon = TAB_ICONS[tab.icon];
const style = {
transform: CSS.Transform.toString(transform),
transition,
WebkitAppRegion: "no-drag",
zIndex: isDragging ? 10 : undefined,
} as React.CSSProperties;
const handleClick = () => {
if (isActive) return;
setActiveTab(tab.id);
// No navigate() — Activity handles visibility
};
const handleClose = (e: React.MouseEvent) => {
e.stopPropagation();
closeTab(tab.id);
// No navigate() — store handles activeTabId switch
};
// Stop pointer down on close so it doesn't start a drag on the parent button.
const stopDragOnClose = (e: React.PointerEvent) => {
e.stopPropagation();
};
return (
<button
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
onClick={handleClick}
className={cn(
"group flex h-7 w-40 items-center gap-1.5 rounded-md px-2 text-xs transition-colors",
"select-none cursor-default",
isActive
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
: "bg-sidebar-accent/50 text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
isDragging && "opacity-60",
)}
>
{Icon && <Icon className="size-3.5 shrink-0" />}
<span
className="min-w-0 flex-1 overflow-hidden whitespace-nowrap text-left"
style={{
maskImage: "linear-gradient(to right, black calc(100% - 12px), transparent)",
WebkitMaskImage: "linear-gradient(to right, black calc(100% - 12px), transparent)",
}}
>
{tab.title}
</span>
{!isOnly && (
<span
onClick={handleClose}
onPointerDown={stopDragOnClose}
className="hidden size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors group-hover:flex hover:bg-muted-foreground/20 hover:text-foreground"
>
<X className="size-2.5" />
</span>
)}
</button>
);
}
function NewTabButton() {
const addTab = useTabStore((s) => s.addTab);
const setActiveTab = useTabStore((s) => s.setActiveTab);
const handleClick = () => {
const path = "/issues";
const tabId = addTab(path, "Issues", resolveRouteIcon(path));
setActiveTab(tabId);
// No navigate() — new tab's router starts at /issues automatically
};
return (
<button
onClick={handleClick}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-muted/50 hover:text-muted-foreground"
>
<Plus className="size-3.5" />
</button>
);
}
export function TabBar() {
const tabs = useTabStore((s) => s.tabs);
const activeTabId = useTabStore((s) => s.activeTabId);
const moveTab = useTabStore((s) => s.moveTab);
// distance: 5 — pointer must move 5px to start a drag, otherwise it's a click.
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
}),
);
const tabIds = tabs.map((t) => t.id);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const from = tabs.findIndex((t) => t.id === active.id);
const to = tabs.findIndex((t) => t.id === over.id);
if (from !== -1 && to !== -1) moveTab(from, to);
};
return (
<div className="flex h-full items-center gap-0.5 px-2 justify-start">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToHorizontalAxis, restrictToParentElement]}
onDragEnd={handleDragEnd}
>
<SortableContext items={tabIds} strategy={horizontalListSortingStrategy}>
{tabs.map((tab) => (
<SortableTabItem
key={tab.id}
tab={tab}
isActive={tab.id === activeTabId}
isOnly={tabs.length === 1}
/>
))}
</SortableContext>
</DndContext>
<NewTabButton />
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { Activity, useEffect } from "react";
import { RouterProvider } from "react-router-dom";
import { useTabStore } from "@/stores/tab-store";
import { TabNavigationProvider } from "@/platform/navigation";
import { useTabRouterSync } from "@/hooks/use-tab-router-sync";
/** Inner wrapper rendered inside each tab's RouterProvider. */
function TabRouterInner({ tabId }: { tabId: string }) {
const tab = useTabStore((s) => s.tabs.find((t) => t.id === tabId));
useTabRouterSync(tabId, tab!.router);
return null;
}
/**
* Renders all tabs using Activity for state preservation.
* Only the active tab is visible; hidden tabs keep their DOM and React state.
*/
export function TabContent() {
const tabs = useTabStore((s) => s.tabs);
const activeTabId = useTabStore((s) => s.activeTabId);
// Sync document.title when switching tabs
useEffect(() => {
const tab = tabs.find((t) => t.id === activeTabId);
if (tab) document.title = tab.title;
}, [activeTabId, tabs]);
return (
<>
{tabs.map((tab) => (
<Activity
key={tab.id}
mode={tab.id === activeTabId ? "visible" : "hidden"}
>
<TabNavigationProvider router={tab.router}>
<RouterProvider router={tab.router} />
<TabRouterInner tabId={tab.id} />
</TabNavigationProvider>
</Activity>
))}
</>
);
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,24 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@multica/ui/styles/tokens.css";
@import "@multica/ui/styles/base.css";
@custom-variant dark (&:is(.dark *));
/* Geist font: define CSS variables that tokens.css @theme inline references.
Web app gets these from next/font/google; desktop must set them explicitly. */
:root {
--font-sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
@source "../../../../../packages/ui/**/*.tsx";
@source "../../../../../packages/core/**/*.{ts,tsx}";
@source "../../../../../packages/views/**/*.{ts,tsx}";
@source "./**/*.tsx";
/* Desktop-specific: override sidebar container padding for traffic light layout */
[data-slot="sidebar-container"] {
padding: 0 !important;
}

View File

@@ -0,0 +1,8 @@
import { useEffect } from "react";
/** Sets document.title. The tab system observes this automatically. */
export function useDocumentTitle(title: string) {
useEffect(() => {
if (title) document.title = title;
}, [title]);
}

View File

@@ -0,0 +1,40 @@
import { useCallback } from "react";
import type { DataRouter } from "react-router-dom";
import { useTabStore } from "@/stores/tab-store";
/**
* Shared hint map so useTabRouterSync can distinguish back vs forward POP.
* Set before calling router.navigate(-1 | 1), read in the synchronous subscription.
*/
export const popDirectionHints = new Map<DataRouter, "back" | "forward">();
/**
* Per-tab back/forward navigation derived from the active tab's history state.
* Replaces the old global useNavigationHistory() hook.
*/
export function useTabHistory() {
// Return the actual tab object from the store — stable reference.
// Do NOT create a new object in the selector (causes infinite re-renders).
const activeTab = useTabStore((s) =>
s.tabs.find((t) => t.id === s.activeTabId),
);
const canGoBack = (activeTab?.historyIndex ?? 0) > 0;
const canGoForward =
(activeTab?.historyIndex ?? 0) < (activeTab?.historyLength ?? 1) - 1;
const goBack = useCallback(() => {
if (!activeTab || activeTab.historyIndex <= 0) return;
popDirectionHints.set(activeTab.router, "back");
activeTab.router.navigate(-1);
}, [activeTab]);
const goForward = useCallback(() => {
if (!activeTab || activeTab.historyIndex >= activeTab.historyLength - 1)
return;
popDirectionHints.set(activeTab.router, "forward");
activeTab.router.navigate(1);
}, [activeTab]);
return { canGoBack, canGoForward, goBack, goForward };
}

View File

@@ -0,0 +1,49 @@
import { useEffect, useRef } from "react";
import type { DataRouter } from "react-router-dom";
import { useTabStore, resolveRouteIcon } from "@/stores/tab-store";
import { popDirectionHints } from "./use-tab-history";
/**
* Subscribe to a tab's memory router and sync path + history tracking
* back into the tab store.
*
* Called once per tab inside its RouterProvider subtree.
*/
export function useTabRouterSync(tabId: string, router: DataRouter) {
const indexRef = useRef(0);
const lengthRef = useRef(1);
useEffect(() => {
// Sync initial state
const initialPath = router.state.location.pathname;
const store = useTabStore.getState();
store.updateTab(tabId, { path: initialPath, icon: resolveRouteIcon(initialPath) });
const unsubscribe = router.subscribe((state) => {
const { pathname } = state.location;
const action = state.historyAction;
if (action === "PUSH") {
indexRef.current += 1;
lengthRef.current = indexRef.current + 1;
} else if (action === "POP") {
// Determine direction from the hint set by goBack/goForward
const hint = popDirectionHints.get(router);
popDirectionHints.delete(router);
if (hint === "forward") {
indexRef.current = Math.min(indexRef.current + 1, lengthRef.current - 1);
} else {
// Default to back
indexRef.current = Math.max(0, indexRef.current - 1);
}
}
// REPLACE: index and length stay the same
const store = useTabStore.getState();
store.updateTab(tabId, { path: pathname, icon: resolveRouteIcon(pathname) });
store.updateTabHistory(tabId, indexRef.current, lengthRef.current);
});
return unsubscribe;
}, [tabId, router]);
}

View File

@@ -0,0 +1,29 @@
import { useEffect } from "react";
import { useTabStore } from "@/stores/tab-store";
/**
* Watches document.title via MutationObserver and updates the active tab's title.
*
* Pages set document.title via TitleSync (route handle.title) or useDocumentTitle().
* This observer picks up the change and syncs it to the tab store.
*/
export function useActiveTitleSync() {
useEffect(() => {
const observer = new MutationObserver(() => {
const title = document.title;
if (!title) return;
const { tabs, activeTabId } = useTabStore.getState();
const activeTab = tabs.find((t) => t.id === activeTabId);
if (activeTab && activeTab.title !== title) {
useTabStore.getState().updateTab(activeTabId, { title });
}
});
const titleEl = document.querySelector("title");
if (titleEl) {
observer.observe(titleEl, { childList: true, characterData: true, subtree: true });
}
return () => observer.disconnect();
}, []);
}

View File

@@ -0,0 +1,11 @@
import ReactDOM from "react-dom/client";
import App from "./App";
import "@fontsource/geist-sans/400.css";
import "@fontsource/geist-sans/500.css";
import "@fontsource/geist-sans/600.css";
import "@fontsource/geist-sans/700.css";
import "@fontsource/geist-mono/400.css";
import "@fontsource/geist-mono/700.css";
import "./globals.css";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -0,0 +1,17 @@
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { IssueDetail } from "@multica/views/issues/components";
import { useWorkspaceId } from "@multica/core/hooks";
import { issueDetailOptions } from "@multica/core/issues/queries";
import { useDocumentTitle } from "@/hooks/use-document-title";
export function IssueDetailPage() {
const { id } = useParams<{ id: string }>();
const wsId = useWorkspaceId();
const { data: issue } = useQuery(issueDetailOptions(wsId, id!));
useDocumentTitle(issue ? `${issue.identifier}: ${issue.title}` : "Issue");
if (!id) return null;
return <IssueDetail issueId={id} />;
}

View File

@@ -0,0 +1,20 @@
import { LoginPage } from "@multica/views/auth";
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
export function DesktopLoginPage() {
return (
<div className="flex h-screen flex-col">
{/* Traffic light inset */}
<div
className="h-[38px] shrink-0"
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
/>
<LoginPage
logo={<MulticaIcon bordered size="lg" />}
onSuccess={() => {
// Auth store update triggers AppContent re-render → shows DesktopShell
}}
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { ProjectDetail } from "@multica/views/projects/components";
import { useWorkspaceId } from "@multica/core/hooks";
import { projectDetailOptions } from "@multica/core/projects/queries";
import { useDocumentTitle } from "@/hooks/use-document-title";
export function ProjectDetailPage() {
const { id } = useParams<{ id: string }>();
const wsId = useWorkspaceId();
const { data: project } = useQuery(projectDetailOptions(wsId, id!));
useDocumentTitle(project ? `${project.icon || "📁"} ${project.title}` : "Project");
if (!id) return null;
return <ProjectDetail projectId={id} />;
}

View File

@@ -0,0 +1,116 @@
import { useEffect, useMemo, useState } from "react";
import type { DataRouter } from "react-router-dom";
import {
NavigationProvider,
type NavigationAdapter,
} from "@multica/views/navigation";
import { useAuthStore } from "@multica/core/auth";
import { useTabStore, resolveRouteIcon } from "@/stores/tab-store";
/**
* Root-level navigation provider for components outside the per-tab RouterProviders
* (sidebar, search dialog, modals, etc.).
*
* Reads from the active tab's memory router via router.subscribe().
* Does NOT use any react-router hooks — it's above all RouterProviders.
*/
export function DesktopNavigationProvider({
children,
}: {
children: React.ReactNode;
}) {
const activeTab = useTabStore((s) => s.tabs.find((t) => t.id === s.activeTabId));
const [pathname, setPathname] = useState(activeTab?.path ?? "/issues");
// Subscribe to the active tab's router for pathname updates
useEffect(() => {
if (!activeTab) return;
setPathname(activeTab.router.state.location.pathname);
return activeTab.router.subscribe((state) => {
setPathname(state.location.pathname);
});
}, [activeTab?.id]); // eslint-disable-line react-hooks/exhaustive-deps
const adapter: NavigationAdapter = useMemo(
() => ({
push: (path: string) => {
if (path === "/login") {
// DashboardGuard token expired — force back to login screen
useAuthStore.getState().logout();
return;
}
const tab = useTabStore.getState().tabs.find(
(t) => t.id === useTabStore.getState().activeTabId,
);
tab?.router.navigate(path);
},
replace: (path: string) => {
const tab = useTabStore.getState().tabs.find(
(t) => t.id === useTabStore.getState().activeTabId,
);
tab?.router.navigate(path, { replace: true });
},
back: () => {
const tab = useTabStore.getState().tabs.find(
(t) => t.id === useTabStore.getState().activeTabId,
);
tab?.router.navigate(-1);
},
pathname,
searchParams: new URLSearchParams(),
openInNewTab: (path: string, title?: string) => {
const icon = resolveRouteIcon(path);
const store = useTabStore.getState();
const tabId = store.openTab(path, title ?? path, icon);
store.setActiveTab(tabId);
},
getShareableUrl: (path: string) => `https://www.multica.ai${path}`,
}),
[pathname],
);
return <NavigationProvider value={adapter}>{children}</NavigationProvider>;
}
/**
* Per-tab navigation provider rendered inside each tab's Activity wrapper.
* Subscribes to the tab's own router for up-to-date pathname.
*
* This is what @multica/views page components read via useNavigation().
*/
export function TabNavigationProvider({
router,
children,
}: {
router: DataRouter;
children: React.ReactNode;
}) {
const [location, setLocation] = useState(router.state.location);
useEffect(() => {
setLocation(router.state.location);
return router.subscribe((state) => {
setLocation(state.location);
});
}, [router]);
const adapter: NavigationAdapter = useMemo(
() => ({
push: (path: string) => router.navigate(path),
replace: (path: string) => router.navigate(path, { replace: true }),
back: () => router.navigate(-1),
pathname: location.pathname,
searchParams: new URLSearchParams(location.search),
openInNewTab: (path: string, title?: string) => {
const icon = resolveRouteIcon(path);
const store = useTabStore.getState();
const newTabId = store.openTab(path, title ?? path, icon);
store.setActiveTab(newTabId);
},
getShareableUrl: (path: string) => `https://www.multica.ai${path}`,
}),
[router, location],
);
return <NavigationProvider value={adapter}>{children}</NavigationProvider>;
}

View File

@@ -0,0 +1,99 @@
import { useEffect } from "react";
import {
createMemoryRouter,
Navigate,
Outlet,
useMatches,
} from "react-router-dom";
import type { RouteObject } from "react-router-dom";
import { IssueDetailPage } from "./pages/issue-detail-page";
import { ProjectDetailPage } from "./pages/project-detail-page";
import { IssuesPage } from "@multica/views/issues/components";
import { ProjectsPage } from "@multica/views/projects/components";
import { MyIssuesPage } from "@multica/views/my-issues";
import { RuntimesPage } from "@multica/views/runtimes";
import { SkillsPage } from "@multica/views/skills";
import { AgentsPage } from "@multica/views/agents";
import { InboxPage } from "@multica/views/inbox";
import { SettingsPage } from "@multica/views/settings";
/**
* Sets document.title from the deepest matched route's handle.title.
* The tab system observes document.title via MutationObserver.
* Pages with dynamic titles (e.g. issue detail) override by setting
* document.title directly via useDocumentTitle().
*/
function TitleSync() {
const matches = useMatches();
const title = [...matches]
.reverse()
.find((m) => (m.handle as { title?: string })?.title)
?.handle as { title?: string } | undefined;
useEffect(() => {
if (title?.title) document.title = title.title;
}, [title?.title]);
return null;
}
/** Wrapper that renders route children + TitleSync */
function PageShell() {
return (
<>
<TitleSync />
<Outlet />
</>
);
}
/** Route definitions shared by all tabs (no layout wrapper). */
export const appRoutes: RouteObject[] = [
{
element: <PageShell />,
children: [
{ index: true, element: <Navigate to="/issues" replace /> },
{ path: "issues", element: <IssuesPage />, handle: { title: "Issues" } },
{
path: "issues/:id",
element: <IssueDetailPage />,
handle: { title: "Issue" },
},
{
path: "projects",
element: <ProjectsPage />,
handle: { title: "Projects" },
},
{
path: "projects/:id",
element: <ProjectDetailPage />,
handle: { title: "Project" },
},
{
path: "my-issues",
element: <MyIssuesPage />,
handle: { title: "My Issues" },
},
{
path: "runtimes",
element: <RuntimesPage />,
handle: { title: "Runtimes" },
},
{ path: "skills", element: <SkillsPage />, handle: { title: "Skills" } },
{ path: "agents", element: <AgentsPage />, handle: { title: "Agents" } },
{ path: "inbox", element: <InboxPage />, handle: { title: "Inbox" } },
{
path: "settings",
element: <SettingsPage />,
handle: { title: "Settings" },
},
],
},
];
/** Create an independent memory router for a tab. */
export function createTabRouter(initialPath: string) {
return createMemoryRouter(appRoutes, {
initialEntries: [initialPath],
});
}

View File

@@ -0,0 +1,195 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { arrayMove } from "@dnd-kit/sortable";
import { createPersistStorage, defaultStorage } from "@multica/core/platform";
import type { DataRouter } from "react-router-dom";
import { createTabRouter } from "../routes";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface Tab {
id: string;
path: string;
title: string;
icon: string;
router: DataRouter;
historyIndex: number;
historyLength: number;
}
interface TabStore {
tabs: Tab[];
activeTabId: string;
/** Open a background tab. Deduplicates by path. Returns the tab id. */
openTab: (path: string, title: string, icon: string) => string;
/** Always create a new tab (no dedup). Returns the tab id. */
addTab: (path: string, title: string, icon: string) => string;
/** Close a tab. Disposes router. */
closeTab: (tabId: string) => void;
/** Switch to a tab by id. */
setActiveTab: (tabId: string) => void;
/** Update a tab's metadata (path, title, icon — partial). */
updateTab: (tabId: string, patch: Partial<Pick<Tab, "path" | "title" | "icon">>) => void;
/** Update a tab's history tracking. */
updateTabHistory: (tabId: string, historyIndex: number, historyLength: number) => void;
/** Reorder tabs by moving one from fromIndex to toIndex. Preserves router/history. */
moveTab: (fromIndex: number, toIndex: number) => void;
}
// ---------------------------------------------------------------------------
// Route → icon mapping (title comes from document.title, not from here)
// ---------------------------------------------------------------------------
const ROUTE_ICONS: Record<string, string> = {
"/inbox": "Inbox",
"/my-issues": "CircleUser",
"/issues": "ListTodo",
"/projects": "FolderKanban",
"/agents": "Bot",
"/runtimes": "Monitor",
"/skills": "BookOpenText",
"/settings": "Settings",
};
/** Resolve a route icon. Title is NOT determined here — it comes from document.title. */
export function resolveRouteIcon(pathname: string): string {
return ROUTE_ICONS[pathname]
?? (pathname.startsWith("/issues/") ? "ListTodo" : undefined)
?? (pathname.startsWith("/projects/") ? "FolderKanban" : undefined)
?? "ListTodo";
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
const DEFAULT_PATH = "/issues";
function createId(): string {
return crypto.randomUUID();
}
function makeTab(path: string, title: string, icon: string): Tab {
return {
id: createId(),
path,
title,
icon,
router: createTabRouter(path),
historyIndex: 0,
historyLength: 1,
};
}
const initialTab = makeTab(DEFAULT_PATH, "Issues", resolveRouteIcon(DEFAULT_PATH));
export const useTabStore = create<TabStore>()(
persist(
(set, get) => ({
tabs: [initialTab],
activeTabId: initialTab.id,
openTab(path, title, icon) {
const { tabs } = get();
const existing = tabs.find((t) => t.path === path);
if (existing) return existing.id;
const tab = makeTab(path, title, icon);
set({ tabs: [...tabs, tab] });
return tab.id;
},
addTab(path, title, icon) {
const tab = makeTab(path, title, icon);
set((s) => ({ tabs: [...s.tabs, tab] }));
return tab.id;
},
closeTab(tabId) {
const { tabs, activeTabId } = get();
const closingTab = tabs.find((t) => t.id === tabId);
// Never close the last tab — replace with default
if (tabs.length === 1) {
closingTab?.router.dispose();
const fresh = makeTab(DEFAULT_PATH, "Issues", resolveRouteIcon(DEFAULT_PATH));
set({ tabs: [fresh], activeTabId: fresh.id });
return;
}
const idx = tabs.findIndex((t) => t.id === tabId);
if (idx === -1) return;
closingTab?.router.dispose();
const next = tabs.filter((t) => t.id !== tabId);
if (tabId === activeTabId) {
const newActive = next[Math.min(idx, next.length - 1)];
set({ tabs: next, activeTabId: newActive.id });
} else {
set({ tabs: next });
}
},
setActiveTab(tabId) {
set({ activeTabId: tabId });
},
updateTab(tabId, patch) {
set((s) => ({
tabs: s.tabs.map((t) =>
t.id === tabId ? { ...t, ...patch } : t,
),
}));
},
updateTabHistory(tabId, historyIndex, historyLength) {
set((s) => ({
tabs: s.tabs.map((t) =>
t.id === tabId ? { ...t, historyIndex, historyLength } : t,
),
}));
},
moveTab(fromIndex, toIndex) {
if (fromIndex === toIndex) return;
set((s) => ({ tabs: arrayMove(s.tabs, fromIndex, toIndex) }));
},
}),
{
name: "multica_tabs",
version: 1,
storage: createJSONStorage(() => createPersistStorage(defaultStorage)),
partialize: (state) => ({
tabs: state.tabs.map(
({ router, historyIndex, historyLength, ...rest }) => rest,
),
activeTabId: state.activeTabId,
}),
merge: (persistedState, currentState) => {
const persisted = persistedState as
| Pick<TabStore, "tabs" | "activeTabId">
| undefined;
if (!persisted?.tabs?.length) return currentState;
const tabs: Tab[] = persisted.tabs.map((tab) => ({
...tab,
router: createTabRouter(tab.path),
historyIndex: 0,
historyLength: 1,
}));
// Validate activeTabId — fall back to first tab if stale
const activeTabId = tabs.some((t) => t.id === persisted.activeTabId)
? persisted.activeTabId
: tabs[0].id;
return { ...currentState, tabs, activeTabId };
},
},
),
);

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
}
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts"
],
"compilerOptions": {
"composite": true,
"noImplicitAny": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": [
"src/renderer/src/*"
]
}
}
}

3
apps/docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.next/
.source/
node_modules/

View File

@@ -0,0 +1,7 @@
import type { ReactNode } from "react";
import { HomeLayout } from "fumadocs-ui/layouts/home";
import { baseOptions } from "@/app/layout.config";
export default function Layout({ children }: { children: ReactNode }) {
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
}

View File

@@ -0,0 +1,29 @@
import Link from "next/link";
export default function HomePage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center gap-6 text-center px-4">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
Multica Documentation
</h1>
<p className="max-w-2xl text-lg text-fd-muted-foreground">
The open-source managed agents platform. Turn coding agents into real
teammates assign tasks, track progress, compound skills.
</p>
<div className="flex gap-4">
<Link
href="/docs"
className="inline-flex items-center rounded-md bg-fd-primary px-6 py-3 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
>
Get Started
</Link>
<Link
href="https://github.com/multica-ai/multica"
className="inline-flex items-center rounded-md border border-fd-border px-6 py-3 text-sm font-medium transition-colors hover:bg-fd-accent"
>
GitHub
</Link>
</div>
</main>
);
}

View File

@@ -0,0 +1,4 @@
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
export const { GET } = createFromSource(source);

View File

@@ -0,0 +1,47 @@
import { source } from "@/lib/source";
import {
DocsPage,
DocsBody,
DocsDescription,
DocsTitle,
} from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { Metadata } from "next";
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX components={{ ...defaultMdxComponents }} />
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
};
}

View File

@@ -0,0 +1,12 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
import { baseOptions } from "@/app/layout.config";
import { source } from "@/lib/source";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions}>
{children}
</DocsLayout>
);
}

3
apps/docs/app/global.css Normal file
View File

@@ -0,0 +1,3 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";

View File

@@ -0,0 +1,25 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { BookOpen, Terminal, Rocket, Code } from "lucide-react";
export const baseOptions: BaseLayoutProps = {
nav: {
title: (
<span className="font-semibold text-base">Multica Docs</span>
),
},
links: [
{
text: "Documentation",
url: "/docs",
active: "nested-url",
},
{
text: "GitHub",
url: "https://github.com/multica-ai/multica",
},
{
text: "Cloud",
url: "https://multica.ai",
},
],
};

23
apps/docs/app/layout.tsx Normal file
View File

@@ -0,0 +1,23 @@
import "./global.css";
import { RootProvider } from "fumadocs-ui/provider";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | Multica Docs",
default: "Multica Docs",
},
description:
"Documentation for Multica — the open-source managed agents platform.",
};
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,90 @@
---
title: CLI Installation
description: Install the Multica CLI and start the agent daemon.
---
## Installation
### Homebrew (macOS/Linux)
```bash
brew tap multica-ai/tap
brew install multica
```
### Build from Source
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make build
cp server/bin/multica /usr/local/bin/multica
```
### Download from GitHub Releases
If Homebrew is not available, download the binary directly:
```bash
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # "darwin" or "linux"
ARCH=$(uname -m) # "x86_64" or "arm64"
# Normalize architecture name
if [ "$ARCH" = "x86_64" ]; then
ARCH="amd64"
fi
# Get the latest release tag from GitHub
LATEST=$(curl -sI https://github.com/multica-ai/multica/releases/latest \
| grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n')
# Download and extract
curl -sL "https://github.com/multica-ai/multica/releases/download/${LATEST}/multica_${OS}_${ARCH}.tar.gz" \
-o /tmp/multica.tar.gz
tar -xzf /tmp/multica.tar.gz -C /tmp multica
sudo mv /tmp/multica /usr/local/bin/multica
rm /tmp/multica.tar.gz
```
### Update
```bash
multica update
```
This auto-detects your installation method (Homebrew or manual) and upgrades accordingly.
## Quick Start
```bash
# 1. Authenticate (opens browser for login)
multica login
# 2. Start the agent daemon
multica daemon start
# 3. Done — agents in your watched workspaces can now execute tasks on your machine
```
`multica login` automatically discovers all workspaces you belong to and adds them to the daemon watch list.
## Verify
```bash
multica daemon status
```
Confirm:
1. Status is `running`
2. At least one agent is listed (e.g. `claude`, `codex`)
3. At least one workspace is being watched
If the agents list is empty, install at least one AI agent CLI:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`)
- [Codex](https://github.com/openai/codex) (`codex`)
Then restart the daemon:
```bash
multica daemon stop && multica daemon start
```

View File

@@ -0,0 +1,4 @@
{
"title": "CLI & Daemon",
"pages": ["installation", "reference"]
}

View File

@@ -0,0 +1,306 @@
---
title: CLI Reference
description: Complete command reference for the Multica CLI and agent daemon.
---
The `multica` CLI connects your local machine to Multica. It handles authentication, workspace management, issue tracking, and runs the agent daemon that executes AI tasks locally.
## Authentication
### Browser Login
```bash
multica login
```
Opens your browser for OAuth authentication, creates a 90-day personal access token, and auto-configures your workspaces.
### Token Login
```bash
multica login --token
```
Authenticate by pasting a personal access token directly. Useful for headless environments.
### Check Status
```bash
multica auth status
```
Shows your current server, user, and token validity.
### Logout
```bash
multica auth logout
```
Removes the stored authentication token.
## Agent Daemon
The daemon is the local agent runtime. It detects available AI CLIs on your machine, registers them with the Multica server, and executes tasks when agents are assigned work.
### Start
```bash
multica daemon start
```
By default, the daemon runs in the background and logs to `~/.multica/daemon.log`.
To run in the foreground (useful for debugging):
```bash
multica daemon start --foreground
```
### Stop
```bash
multica daemon stop
```
### Status
```bash
multica daemon status
multica daemon status --output json
```
Shows PID, uptime, detected agents, and watched workspaces.
### Logs
```bash
multica daemon logs # Last 50 lines
multica daemon logs -f # Follow (tail -f)
multica daemon logs -n 100 # Last 100 lines
```
### Supported Agents
The daemon auto-detects these AI CLIs on your PATH:
| CLI | Command | Description |
|-----|---------|-------------|
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `claude` | Anthropic's coding agent |
| [Codex](https://github.com/openai/codex) | `codex` | OpenAI's coding agent |
You need at least one installed. The daemon registers each detected CLI as an available runtime.
### How It Works
1. On start, the daemon detects installed agent CLIs and registers a runtime for each agent in each watched workspace
2. It polls the server at a configurable interval (default: 3s) for claimed tasks
3. When a task arrives, it creates an isolated workspace directory, spawns the agent CLI, and streams results back
4. Heartbeats are sent periodically (default: 15s) so the server knows the daemon is alive
5. On shutdown, all runtimes are deregistered
### Configuration
Daemon behavior is configured via flags or environment variables:
| Setting | Flag | Env Variable | Default |
|---------|------|--------------|---------|
| Poll interval | `--poll-interval` | `MULTICA_DAEMON_POLL_INTERVAL` | `3s` |
| Heartbeat interval | `--heartbeat-interval` | `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` |
| Agent timeout | `--agent-timeout` | `MULTICA_AGENT_TIMEOUT` | `2h` |
| Max concurrent tasks | `--max-concurrent-tasks` | `MULTICA_DAEMON_MAX_CONCURRENT_TASKS` | `20` |
| Daemon ID | `--daemon-id` | `MULTICA_DAEMON_ID` | hostname |
| Device name | `--device-name` | `MULTICA_DAEMON_DEVICE_NAME` | hostname |
| Runtime name | `--runtime-name` | `MULTICA_AGENT_RUNTIME_NAME` | `Local Agent` |
| Workspaces root | — | `MULTICA_WORKSPACES_ROOT` | `~/multica_workspaces` |
Agent-specific overrides:
| Variable | Description |
|----------|-------------|
| `MULTICA_CLAUDE_PATH` | Custom path to the `claude` binary |
| `MULTICA_CLAUDE_MODEL` | Override the Claude model used |
| `MULTICA_CODEX_PATH` | Custom path to the `codex` binary |
| `MULTICA_CODEX_MODEL` | Override the Codex model used |
### Self-Hosted Server
When connecting to a self-hosted Multica instance, point the CLI to your server before logging in:
```bash
export MULTICA_APP_URL=https://app.example.com
export MULTICA_SERVER_URL=wss://api.example.com/ws
multica login
multica daemon start
```
Or set them persistently:
```bash
multica config set app_url https://app.example.com
multica config set server_url wss://api.example.com/ws
```
### Profiles
Profiles let you run multiple daemons on the same machine — for example, one for production and one for a staging server.
```bash
# Start a daemon for the staging server
multica --profile staging login
multica --profile staging daemon start
# Default profile runs separately
multica daemon start
```
Each profile gets its own config directory (`~/.multica/profiles/<name>/`), daemon state, health port, and workspace root.
## Workspaces
### List Workspaces
```bash
multica workspace list
```
Watched workspaces are marked with `*`. The daemon only processes tasks for watched workspaces.
### Watch / Unwatch
```bash
multica workspace watch <workspace-id>
multica workspace unwatch <workspace-id>
```
### Get Details
```bash
multica workspace get <workspace-id>
multica workspace get <workspace-id> --output json
```
### List Members
```bash
multica workspace members <workspace-id>
```
## Issues
### List Issues
```bash
multica issue list
multica issue list --status in_progress
multica issue list --priority urgent --assignee "Agent Name"
multica issue list --limit 20 --output json
```
Available filters: `--status`, `--priority`, `--assignee`, `--limit`.
### Get Issue
```bash
multica issue get <id>
multica issue get <id> --output json
```
### Create Issue
```bash
multica issue create --title "Fix login bug" --description "..." --priority high --assignee "Lambda"
```
Flags: `--title` (required), `--description`, `--status`, `--priority`, `--assignee`, `--parent`, `--due-date`.
### Update Issue
```bash
multica issue update <id> --title "New title" --priority urgent
```
### Assign Issue
```bash
multica issue assign <id> --to "Lambda"
multica issue assign <id> --unassign
```
### Change Status
```bash
multica issue status <id> in_progress
```
Valid statuses: `backlog`, `todo`, `in_progress`, `in_review`, `done`, `blocked`, `cancelled`.
### Comments
```bash
# List comments
multica issue comment list <issue-id>
# Add a comment
multica issue comment add <issue-id> --content "Looks good, merging now"
# Reply to a specific comment
multica issue comment add <issue-id> --parent <comment-id> --content "Thanks!"
# Delete a comment
multica issue comment delete <comment-id>
```
### Execution History
```bash
# List all execution runs for an issue
multica issue runs <issue-id>
multica issue runs <issue-id> --output json
# View messages for a specific execution run
multica issue run-messages <task-id>
multica issue run-messages <task-id> --output json
# Incremental fetch (only messages after a given sequence number)
multica issue run-messages <task-id> --since 42 --output json
```
## Configuration
### View Config
```bash
multica config show
```
Shows config file path, server URL, app URL, and default workspace.
### Set Values
```bash
multica config set server_url wss://api.example.com/ws
multica config set app_url https://app.example.com
multica config set workspace_id <workspace-id>
```
## Other Commands
```bash
multica version # Show CLI version and commit hash
multica update # Update to latest version
multica agent list # List agents in the current workspace
```
## Output Formats
Most commands support `--output` with two formats:
- `table` — human-readable table (default for list commands)
- `json` — structured JSON (useful for scripting and automation)
```bash
multica issue list --output json
multica daemon status --output json
```

View File

@@ -0,0 +1,75 @@
---
title: Architecture
description: Technical architecture of the Multica platform.
---
## Overview
Multica is a Go backend + monorepo frontend (pnpm workspaces + Turborepo) with shared packages.
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go Backend │────>│ PostgreSQL │
│ Frontend │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (runs on your machine)
│Claude/Codex/ │
│OpenClaw/Code │
└──────────────┘
```
## Project Structure
| Directory | Purpose | Technology |
|-----------|---------|------------|
| `server/` | Go backend | Chi router, sqlc for DB, gorilla/websocket |
| `apps/web/` | Next.js frontend | App Router |
| `apps/desktop/` | Electron desktop app | electron-vite |
| `apps/docs/` | Documentation site | Fumadocs |
| `packages/core/` | Headless business logic | Zero react-dom, all-platform reuse |
| `packages/ui/` | Atomic UI components | Zero business logic, shadcn-based |
| `packages/views/` | Shared business pages | Zero next/\*, zero react-router imports |
| `packages/tsconfig/` | Shared TypeScript config | — |
| `packages/eslint-config/` | Shared ESLint config | — |
## Backend Structure
- **Entry points** (`cmd/`): `server` (HTTP API), `multica` (CLI + daemon), `migrate`
- **Handlers** (`internal/handler/`): One file per domain (issue, comment, agent, auth, daemon)
- **Real-time** (`internal/realtime/`): Hub manages WebSocket clients, server broadcasts events
- **Auth** (`internal/auth/` + `internal/middleware/`): JWT (HS256), middleware sets `X-User-ID` and `X-User-Email` headers
- **Task lifecycle** (`internal/service/task.go`): enqueue → claim → start → complete/fail
- **Agent SDK** (`pkg/agent/`): Unified `Backend` interface for executing prompts via Claude Code or Codex
- **Daemon** (`internal/daemon/`): Auto-detects CLIs, registers runtimes, polls for tasks
- **Database**: PostgreSQL 17 with pgvector, sqlc generates code from SQL in `pkg/db/queries/`
## Frontend Architecture
### Internal Packages Pattern
All shared packages export raw `.ts`/`.tsx` files (no pre-compilation). The consuming app's bundler compiles them directly. This gives zero-config HMR and instant go-to-definition.
### Package Boundaries
- `packages/core/` — zero react-dom, zero localStorage, zero UI libs. All Zustand stores live here.
- `packages/ui/` — pure UI components, zero business logic.
- `packages/views/` — zero `next/*`, zero `react-router-dom`. Uses `NavigationAdapter` for routing.
### State Management
- **TanStack Query** owns all server state (issues, users, workspaces)
- **Zustand** owns all client state (UI selections, filters, drafts)
- **React Context** reserved for cross-cutting plumbing (`WorkspaceIdProvider`, `NavigationProvider`)
### Data Flow
```
Browser → ApiClient (shared/api) → REST API (Chi handlers) → sqlc queries → PostgreSQL
Browser ← WSClient (shared/api) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskService
```
## Multi-tenancy
All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace.

View File

@@ -0,0 +1,178 @@
---
title: Contributing
description: Local development workflow for contributors working on the Multica codebase.
---
## Development Model
Local development uses one shared PostgreSQL container and one database per checkout.
- The main checkout usually uses `.env` and `POSTGRES_DB=multica`
- Each Git worktree uses its own `.env.worktree`
- Every checkout connects to the same PostgreSQL host: `localhost:5432`
- Isolation happens at the database level, not by starting a separate Docker Compose project
- Backend and frontend ports are still unique per worktree
## Prerequisites
- Node.js `v20+`
- `pnpm` `v10.28+`
- Go `v1.26+`
- Docker
## First-Time Setup
### Main Checkout
```bash
cp .env.example .env
make setup-main
```
What `make setup-main` does:
- Installs JavaScript dependencies with `pnpm install`
- Ensures the shared PostgreSQL container is running
- Creates the application database if it does not exist
- Runs all migrations against that database
Start the app:
```bash
make start-main
```
### Worktree
From the worktree directory:
```bash
make worktree-env
make setup-worktree
```
Start the worktree app:
```bash
make start-worktree
```
## Daily Workflow
### Main Checkout
```bash
make start-main
make stop-main
make check-main
```
### Feature Worktree
```bash
git worktree add ../multica-feature -b feat/my-change main
cd ../multica-feature
make worktree-env
make setup-worktree
make start-worktree
```
Day-to-day:
```bash
make start-worktree
make stop-worktree
make check-worktree
```
## Running Main and Worktree Simultaneously
This is a first-class workflow. Both checkouts use the same PostgreSQL container but different databases and ports:
| | Main | Worktree |
|---|---|---|
| Database | `multica` | `multica_my_feature_702` |
| Backend port | `8080` | generated (e.g. `18782`) |
| Frontend port | `3000` | generated (e.g. `13702`) |
## Commands
```bash
# Frontend (all commands go through Turborepo)
pnpm install
pnpm dev:web # Next.js dev server (port 3000)
pnpm dev:desktop # Electron dev (electron-vite, HMR)
pnpm build # Build all frontend apps
pnpm typecheck # TypeScript check
pnpm lint # ESLint
pnpm test # TS tests (Vitest)
# Backend (Go)
make dev # Run Go server (port 8080)
make daemon # Run local daemon
make build # Build server + CLI binaries
make test # Go tests
make sqlc # Regenerate sqlc code
make migrate-up # Run database migrations
make migrate-down # Rollback migrations
```
## Testing
Run all local checks:
```bash
make check
```
This runs:
1. TypeScript typecheck
2. TypeScript unit tests
3. Go tests
4. Playwright E2E tests
## Troubleshooting
### Missing Env File
Create the expected env file:
```bash
# Main checkout
cp .env.example .env
# Worktree
make worktree-env
```
### Check Which Database a Checkout Uses
```bash
cat .env # or .env.worktree
```
Look for `POSTGRES_DB`, `DATABASE_URL`, `PORT`, `FRONTEND_PORT`.
### List All Local Databases
```bash
docker compose exec -T postgres psql -U multica -d postgres \
-At -c "select datname from pg_database order by datname;"
```
### Destructive Reset
Stop PostgreSQL and keep local databases:
```bash
make db-down
```
Wipe all local PostgreSQL data:
```bash
docker compose down -v
```
> **Warning:** This deletes the shared Docker volume and all databases. After that you must run `make setup-main` or `make setup-worktree` again.

View File

@@ -0,0 +1,4 @@
{
"title": "Developers",
"pages": ["contributing", "architecture"]
}

View File

@@ -0,0 +1,48 @@
---
title: Cloud Quickstart
description: Get started with Multica Cloud — no setup required.
---
The fastest way to get started with Multica — no setup required.
## 1. Sign up
Go to [multica.ai](https://multica.ai) and create an account.
## 2. Install the CLI and start the daemon
Give this instruction to your AI agent (Claude Code, Codex, OpenClaw, OpenCode, etc.):
```
Fetch https://github.com/multica-ai/multica/blob/main/CLI_INSTALL.md and follow the instructions to install Multica CLI, log in, and start the daemon on this machine.
```
Or install manually:
```bash
# Install
brew tap multica-ai/tap
brew install multica
# Authenticate and start
multica login
multica daemon start
```
The daemon auto-detects available agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) on your PATH. When an agent is assigned a task, the daemon creates an isolated environment, runs the agent, and reports results back.
## 3. Verify your runtime
Open your workspace in the Multica web app. Navigate to **Settings → Runtimes** — you should see your machine listed as an active **Runtime**.
> **What is a Runtime?** A Runtime is a compute environment that can execute agent tasks. It can be your local machine (via the daemon) or a cloud instance. Each runtime reports which agent CLIs are available, so Multica knows where to route work.
## 4. Create an agent
Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, or OpenCode). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
## 5. Assign your first task
Create an issue from the board (or via `multica issue create`), then assign it to your new agent. The agent will automatically pick up the task, execute it on your runtime, and report progress — just like a human teammate.
That's it! Your agent is now part of the team.

View File

@@ -0,0 +1,4 @@
{
"title": "Getting Started",
"pages": ["cloud-quickstart", "self-hosting"]
}

View File

@@ -0,0 +1,370 @@
---
title: Self-Hosting Guide
description: Deploy Multica on your own infrastructure.
---
## Architecture Overview
Multica has three components:
| Component | Description | Technology |
|-----------|-------------|------------|
| **Backend** | REST API + WebSocket server | Go (single binary) |
| **Frontend** | Web application | Next.js 16 |
| **Database** | Primary data store | PostgreSQL 17 with pgvector |
Each user who wants to run AI agents locally also installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
## Prerequisites
- Docker and Docker Compose
## Quick Install
One command to set up everything:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
```
This clones the repo, starts all services, installs the CLI, and configures everything. Then:
1. Open http://localhost:3000 — log in with any email + code **`888888`**
2. Run `multica login` and `multica daemon start`
<Callout>
For a step-by-step setup, see below.
</Callout>
## Step-by-Step Setup
### Step 1 — Start the Server
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
```
`make selfhost` automatically creates `.env`, generates a random `JWT_SECRET`, and starts all services via Docker Compose.
Once ready:
- **Frontend:** http://localhost:3000
- **Backend API:** http://localhost:8080
<Callout>
If you prefer running the Docker Compose steps manually: `cp .env.example .env`, edit `JWT_SECRET`, then `docker compose -f docker-compose.selfhost.yml up -d`.
</Callout>
### Step 2 — Log In
Open http://localhost:3000. Enter any email address and use verification code **`888888`** to log in.
<Callout>
This master code works in all non-production environments (when `APP_ENV` is not set to `production`). For production, configure an email provider — see [Configuration](#configuration) below.
</Callout>
### Step 3 — Install CLI & Start Daemon
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them with the server, and executes tasks.
### a) Install the CLI and an AI agent
```bash
brew install multica-ai/tap/multica
```
You also need at least one AI agent CLI:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
### b) One-command setup
```bash
multica setup --local
```
This automatically:
1. Configures the CLI to connect to `localhost`
2. Opens your browser for authentication
3. Discovers your workspaces
4. Starts the daemon in the background
Verify the daemon is running:
```bash
multica daemon status
```
<Callout>
Alternatively, configure manually: `multica config local && multica login && multica daemon start`
</Callout>
### Step 4 — Verify & Start Using
1. Open your workspace at http://localhost:3000
2. Navigate to **Settings → Runtimes** — you should see your machine listed
3. Go to **Settings → Agents** and create a new agent
4. Create an issue and assign it to your agent
## Stopping Services
```bash
# Stop Docker Compose services
make selfhost-stop
# Stop the local daemon
multica daemon stop
```
## Switching to Multica Cloud
If you've been self-hosting and want to switch your CLI to [Multica Cloud](https://multica.ai):
```bash
multica config set server_url https://api.multica.ai
multica config set app_url https://multica.ai
multica login
```
Or re-run the install script without `--local` — it will reconfigure the CLI automatically:
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
<Callout>
Your local Docker services are unaffected. Stop them separately if you no longer need them.
</Callout>
## Rebuilding After Updates
```bash
git pull
make selfhost
```
Migrations run automatically on backend startup.
---
## Configuration
All configuration is done via environment variables. Copy `.env.example` as a starting point.
### Required Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | PostgreSQL connection string | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` |
| `JWT_SECRET` | **Must change from default.** Secret key for signing JWT tokens. Use a long random string. | `openssl rand -hex 32` |
| `FRONTEND_ORIGIN` | URL where the frontend is served (used for CORS) | `https://app.example.com` |
### Email (Required for Authentication)
Multica uses email-based magic link authentication via [Resend](https://resend.com).
| Variable | Description |
|----------|-------------|
| `RESEND_API_KEY` | Your Resend API key |
| `RESEND_FROM_EMAIL` | Sender email address (default: `noreply@multica.ai`) |
### Google OAuth (Optional)
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
| `GOOGLE_REDIRECT_URI` | OAuth callback URL (e.g. `https://app.example.com/auth/callback`) |
### File Storage (Optional)
For file uploads and attachments, configure S3 and CloudFront:
| Variable | Description |
|----------|-------------|
| `S3_BUCKET` | S3 bucket name |
| `S3_REGION` | AWS region (default: `us-west-2`) |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
| `CLOUDFRONT_PRIVATE_KEY` | CloudFront private key (PEM format) |
| `COOKIE_DOMAIN` | Domain for CloudFront auth cookies |
### Server
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `8080` | Backend server port |
| `FRONTEND_PORT` | `3000` | Frontend port |
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins |
| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
### CLI / Daemon
These are configured on each user's machine, not on the server:
| Variable | Default | Description |
|----------|---------|-------------|
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | WebSocket URL for daemon → server connection |
| `MULTICA_APP_URL` | `http://localhost:3000` | Frontend URL for CLI login flow |
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | How often the daemon polls for tasks |
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | Heartbeat frequency |
## Database Setup
Multica requires PostgreSQL 17 with the pgvector extension.
### Using the Included Docker Compose
```bash
docker compose up -d postgres
```
This starts a `pgvector/pgvector:pg17` container on port 5432 with default credentials (`multica`/`multica`).
### Using Your Own PostgreSQL
Ensure the pgvector extension is available:
```sql
CREATE EXTENSION IF NOT EXISTS vector;
```
### Running Migrations
Migrations must be run before starting the server:
```bash
# Using the built binary
./server/bin/migrate up
# Or from source
cd server && go run ./cmd/migrate up
```
## Manual Setup (Without Docker Compose)
If you prefer to build and run services manually:
**Prerequisites:** Go 1.26+, Node.js 20+, pnpm 10.28+, PostgreSQL 17 with pgvector.
```bash
# Start your PostgreSQL (or use: docker compose up -d postgres)
# Build the backend
make build
# Run database migrations
DATABASE_URL="your-database-url" ./server/bin/migrate up
# Start the backend server
DATABASE_URL="your-database-url" PORT=8080 JWT_SECRET="your-secret" ./server/bin/server
```
For the frontend:
```bash
pnpm install
pnpm build
# Start the frontend (production mode)
cd apps/web
REMOTE_API_URL=http://localhost:8080 pnpm start
```
## Reverse Proxy
In production, put a reverse proxy in front of both the backend and frontend to handle TLS and routing.
### Caddy (Recommended)
```
app.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8080
}
```
### Nginx
```nginx
# Frontend
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Backend API
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket support
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}
```
When using separate domains for frontend and backend, set these environment variables accordingly:
```bash
# Backend
FRONTEND_ORIGIN=https://app.example.com
CORS_ALLOWED_ORIGINS=https://app.example.com
# Frontend
REMOTE_API_URL=https://api.example.com
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_WS_URL=wss://api.example.com/ws
```
## Health Check
The backend exposes a health check endpoint:
```
GET /health
→ {"status":"ok"}
```
Use this for load balancer health checks or monitoring.
## Upgrading
1. Pull the latest code or image
2. Run migrations: `./server/bin/migrate up`
3. Restart the backend and frontend
Migrations are forward-only and safe to run on a live database. They are idempotent — running them multiple times has no effect.

View File

@@ -0,0 +1,50 @@
---
title: Agents
description: How AI agents work in Multica — execution model, skills, and runtime guidelines.
---
## Agents as Teammates
In Multica, agents are first-class citizens. They have profiles, show up on the board, post comments, create issues, and report blockers proactively.
Assignees are polymorphic — an issue can be assigned to a member or an agent. The `assignee_type` + `assignee_id` fields on issues distinguish between the two. Agents render with distinct styling (purple background, robot icon).
## Agent Execution Model
When an agent is assigned a task in Multica:
1. The daemon detects the task assignment
2. It creates an isolated workspace directory
3. It spawns the appropriate agent CLI (Claude Code, Codex, OpenClaw, or OpenCode)
4. The agent executes autonomously, streaming progress back to Multica
5. Results are reported — success, failure, or blockers
The full task lifecycle is: **enqueue → claim → start → complete/fail**.
Real-time progress is streamed via WebSocket so you can follow along in the Multica UI.
## Supported Agent Providers
| Provider | CLI Command | Description |
|----------|-------------|-------------|
| Claude Code | `claude` | Anthropic's coding agent |
| Codex | `codex` | OpenAI's coding agent |
| OpenClaw | `openclaw` | Open-source coding agent |
| OpenCode | `opencode` | Open-source coding agent |
The daemon auto-detects which CLIs are available on your PATH and registers them as available runtimes.
## Reusable Skills
Every solution an agent creates can become a reusable skill for the whole team. Skills compound your team's capabilities over time:
- Deployments
- Migrations
- Code reviews
- Common patterns
Skills are shared across the workspace, so any agent (or human) can leverage them.
## Multi-Workspace Support
Each workspace has its own set of agents, issues, and settings. The daemon can watch multiple workspaces simultaneously, routing tasks to the appropriate agent based on workspace configuration.

View File

@@ -0,0 +1,4 @@
{
"title": "Guides",
"pages": ["quickstart", "agents"]
}

View File

@@ -0,0 +1,31 @@
---
title: Quickstart
description: Assign your first task to an agent in under 5 minutes.
---
Once you have the CLI installed (or signed up for [Multica Cloud](https://multica.ai)), follow these steps to assign your first task to an agent.
## 1. Log in and start the daemon
```bash
multica login # Authenticate with your Multica account
multica daemon start # Start the local agent runtime
```
The daemon runs in the background and keeps your machine connected to Multica. It auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) available on your PATH.
## 2. Verify your runtime
Open your workspace in the Multica web app. Navigate to **Settings → Runtimes** — you should see your machine listed as an active **Runtime**.
> **What is a Runtime?** A Runtime is a compute environment that can execute agent tasks. It can be your local machine (via the daemon) or a cloud instance. Each runtime reports which agent CLIs are available, so Multica knows where to route work.
## 3. Create an agent
Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, or OpenCode). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
## 4. Assign your first task
Create an issue from the board (or via `multica issue create`), then assign it to your new agent. The agent will automatically pick up the task, execute it on your runtime, and report progress — just like a human teammate.
That's it! Your agent is now part of the team.

View File

@@ -0,0 +1,47 @@
---
title: Introduction
description: Multica — the open-source managed agents platform. Turn coding agents into real teammates.
---
## What is Multica?
Multica turns coding agents into real teammates. Assign issues to an agent like you'd assign to a colleague — they'll pick up the work, write code, report blockers, and update statuses autonomously.
No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **OpenClaw**, and **OpenCode**.
## Features
- **Agents as Teammates** — assign to an agent like you'd assign to a colleague. They have profiles, show up on the board, post comments, create issues, and report blockers proactively.
- **Autonomous Execution** — set it and forget it. Full task lifecycle management (enqueue, claim, start, complete/fail) with real-time progress streaming via WebSocket.
- **Reusable Skills** — every solution becomes a reusable skill for the whole team. Deployments, migrations, code reviews — skills compound your team's capabilities over time.
- **Unified Runtimes** — one dashboard for all your compute. Local daemons and cloud runtimes, auto-detection of available CLIs, real-time monitoring.
- **Multi-Workspace** — organize work across teams with workspace-level isolation. Each workspace has its own agents, issues, and settings.
## Architecture
| Layer | Stack |
|-------|-------|
| Frontend | Next.js 16 (App Router) |
| Backend | Go (Chi router, sqlc, gorilla/websocket) |
| Database | PostgreSQL 17 with pgvector |
| Agent Runtime | Local daemon executing Claude Code, Codex, OpenClaw, or OpenCode |
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go Backend │────>│ PostgreSQL │
│ Frontend │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (runs on your machine)
│Claude/Codex/ │
│OpenClaw/Code │
└──────────────┘
```
## Next Steps
- [Cloud Quickstart](/docs/getting-started/cloud-quickstart)
- [Self-Hosting](/docs/getting-started/self-hosting)
- [CLI Installation](/docs/cli/installation)
- [Contributing](/docs/developers/contributing)

View File

@@ -0,0 +1,10 @@
{
"title": "Documentation",
"pages": [
"index",
"getting-started",
"cli",
"guides",
"developers"
]
}

7
apps/docs/lib/source.ts Normal file
View File

@@ -0,0 +1,7 @@
import { docs } from "@/.source";
import { loader } from "fumadocs-core/source";
export const source = loader({
baseUrl: "/docs",
source: docs.toFumadocsSource(),
});

6
apps/docs/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

10
apps/docs/next.config.mjs Normal file
View File

@@ -0,0 +1,10 @@
import { createMDX } from "fumadocs-mdx/next";
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
};
export default withMDX(config);

29
apps/docs/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "@multica/docs",
"version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --port 4000",
"build": "fumadocs-mdx && next build",
"start": "next start",
"typecheck": "fumadocs-mdx && tsc --noEmit",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"fumadocs-core": "^15.5.2",
"fumadocs-mdx": "^12.0.3",
"fumadocs-ui": "^15.5.2",
"lucide-react": "catalog:",
"next": "^15.3.3",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@tailwindcss/postcss": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1,9 @@
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
export const docs = defineDocs({
dir: "content/docs",
});
export default defineConfig({
mdxOptions: {},
});

48
apps/docs/tsconfig.json Normal file
View File

@@ -0,0 +1,48 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"noEmit": true,
"allowJs": true,
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
".source/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -2,36 +2,59 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
const { mockSendCode, mockVerifyCode, mockHydrateWorkspace } = vi.hoisted(
() => ({
mockSendCode: vi.fn(),
mockVerifyCode: vi.fn(),
mockHydrateWorkspace: vi.fn(),
}),
);
// Mock next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => "/login",
useSearchParams: () => new URLSearchParams(),
}));
// Mock auth store
const mockLogin = vi.fn();
vi.mock("@/features/auth", () => ({
useAuthStore: (selector: (s: any) => any) =>
selector({
login: mockLogin,
isLoading: false,
}),
// Mock auth store — shared LoginPage uses getState().sendCode/verifyCode,
// web wrapper uses useAuthStore((s) => s.user/isLoading)
vi.mock("@multica/core/auth", () => {
const authState = {
sendCode: mockSendCode,
verifyCode: mockVerifyCode,
user: null,
isLoading: false,
};
const useAuthStore = Object.assign(
(selector: (s: typeof authState) => unknown) => selector(authState),
{ getState: () => authState },
);
return { useAuthStore };
});
// Mock auth-cookie
vi.mock("@/features/auth/auth-cookie", () => ({
setLoggedInCookie: vi.fn(),
}));
// Mock workspace store
const mockHydrateWorkspace = vi.fn();
vi.mock("@/features/workspace", () => ({
useWorkspaceStore: (selector: (s: any) => any) =>
selector({
hydrateWorkspace: mockHydrateWorkspace,
}),
}));
// Mock workspace store — shared LoginPage uses getState().hydrateWorkspace
vi.mock("@multica/core/workspace", () => {
const wsState = { hydrateWorkspace: mockHydrateWorkspace };
const useWorkspaceStore = Object.assign(
(selector: (s: typeof wsState) => unknown) => selector(wsState),
{ getState: () => wsState },
);
return { useWorkspaceStore };
});
// Mock api
vi.mock("@/shared/api", () => ({
vi.mock("@multica/core/api", () => ({
api: {
listWorkspaces: vi.fn().mockResolvedValue([]),
verifyCode: vi.fn(),
setToken: vi.fn(),
getMe: vi.fn(),
},
}));
@@ -42,78 +65,74 @@ describe("LoginPage", () => {
vi.clearAllMocks();
});
it("renders login form with heading, inputs, and button", () => {
it("renders login form with email input and continue button", () => {
render(<LoginPage />);
expect(screen.getByText("Multica")).toBeInTheDocument();
expect(screen.getByText("AI-native task management")).toBeInTheDocument();
expect(screen.getByText("Sign in to Multica")).toBeInTheDocument();
expect(screen.getByText("Enter your email to get a login code")).toBeInTheDocument();
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /sign in/i })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Continue" })
).toBeInTheDocument();
});
it("does not call login when email is empty", async () => {
it("does not call sendCode when email is empty", async () => {
const user = userEvent.setup();
render(<LoginPage />);
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(mockLogin).not.toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "Continue" }));
expect(mockSendCode).not.toHaveBeenCalled();
});
it("calls login with correct args on submit", async () => {
mockLogin.mockResolvedValueOnce({ id: "u1", name: "Test User" });
mockHydrateWorkspace.mockResolvedValueOnce(null);
it("calls sendCode with email on submit", async () => {
mockSendCode.mockResolvedValueOnce(undefined);
const user = userEvent.setup();
render(<LoginPage />);
await user.type(screen.getByLabelText("Email"), "test@multica.ai");
await user.type(screen.getByLabelText("Name"), "Test User");
await user.click(screen.getByRole("button", { name: "Sign in" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith("test@multica.ai", "Test User");
expect(mockSendCode).toHaveBeenCalledWith("test@multica.ai");
});
});
it("calls login with email only when name is empty", async () => {
mockLogin.mockResolvedValueOnce({ id: "u1", name: "" });
mockHydrateWorkspace.mockResolvedValueOnce(null);
it("shows 'Sending code...' while submitting", async () => {
mockSendCode.mockReturnValueOnce(new Promise(() => {}));
const user = userEvent.setup();
render(<LoginPage />);
await user.type(screen.getByLabelText("Email"), "test@multica.ai");
await user.click(screen.getByRole("button", { name: "Sign in" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith("test@multica.ai", undefined);
expect(screen.getByText("Sending code...")).toBeInTheDocument();
});
});
it("shows 'Signing in...' while submitting", async () => {
mockLogin.mockReturnValueOnce(new Promise(() => {}));
it("shows verification code step after sending code", async () => {
mockSendCode.mockResolvedValueOnce(undefined);
const user = userEvent.setup();
render(<LoginPage />);
await user.type(screen.getByLabelText("Email"), "test@multica.ai");
await user.click(screen.getByRole("button", { name: "Sign in" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => {
expect(screen.getByText("Signing in...")).toBeInTheDocument();
expect(screen.getByText("Check your email")).toBeInTheDocument();
});
});
it("shows error when login fails", async () => {
mockLogin.mockRejectedValueOnce(new Error("Network error"));
it("shows error when sendCode fails", async () => {
mockSendCode.mockRejectedValueOnce(new Error("Network error"));
const user = userEvent.setup();
render(<LoginPage />);
await user.type(screen.getByLabelText("Email"), "test@multica.ai");
await user.click(screen.getByRole("button", { name: "Sign in" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => {
expect(
screen.getByText("Login failed. Make sure the server is running."),
).toBeInTheDocument();
expect(screen.getByText("Network error")).toBeInTheDocument();
});
});
});

View File

@@ -1,104 +1,58 @@
"use client";
import { Suspense, useState } from "react";
import { Suspense, useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { api } from "@/shared/api";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useAuthStore } from "@multica/core/auth";
import { setLoggedInCookie } from "@/features/auth/auth-cookie";
import { LoginPage, validateCliCallback } from "@multica/views/auth";
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
function LoginPageContent() {
const router = useRouter();
const login = useAuthStore((s) => s.login);
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace);
const searchParams = useSearchParams();
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) {
setError("Email is required");
return;
const cliCallbackRaw = searchParams.get("cli_callback");
const cliState = searchParams.get("cli_state") || "";
const nextUrl = searchParams.get("next") || "/issues";
// Already authenticated — redirect to dashboard (skip if CLI callback)
useEffect(() => {
if (!isLoading && user && !cliCallbackRaw) {
router.replace(nextUrl);
}
setError("");
setSubmitting(true);
try {
await login(email, name || undefined);
const wsList = await api.listWorkspaces();
await hydrateWorkspace(wsList);
router.push(searchParams.get("next") || "/issues");
} catch (err) {
setError("Login failed. Make sure the server is running.");
setSubmitting(false);
}
};
}, [isLoading, user, router, nextUrl, cliCallbackRaw]);
const lastWorkspaceId =
typeof window !== "undefined"
? localStorage.getItem("multica_workspace_id")
: null;
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Multica</CardTitle>
<CardDescription>AI-native task management</CardDescription>
</CardHeader>
<CardContent>
<form id="login-form" onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="Your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
</form>
</CardContent>
<CardFooter>
<Button
type="submit"
form="login-form"
disabled={submitting || isLoading}
className="w-full"
size="lg"
>
{submitting ? "Signing in..." : "Sign in"}
</Button>
</CardFooter>
</Card>
</div>
<LoginPage
onSuccess={() => router.push(nextUrl)}
google={
googleClientId
? {
clientId: googleClientId,
redirectUri: `${window.location.origin}/auth/callback`,
}
: undefined
}
cliCallback={
cliCallbackRaw && validateCliCallback(cliCallbackRaw)
? { url: cliCallbackRaw, state: cliState }
: undefined
}
lastWorkspaceId={lastWorkspaceId}
onTokenObtained={setLoggedInCookie}
/>
);
}
export default function LoginPage() {
export default function Page() {
return (
<Suspense fallback={null}>
<LoginPageContent />

View File

@@ -1,232 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
Inbox,
ListTodo,
Bot,
BookOpen,
ChevronDown,
Settings,
LogOut,
Plus,
Check,
Sparkles,
Search,
SquarePen,
} from "lucide-react";
import { WorkspaceAvatar } from "@/features/workspace";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useInboxStore } from "@/features/inbox";
import { useModalStore } from "@/features/modals";
const navItems = [
{ href: "/inbox", label: "Inbox", icon: Inbox },
{ href: "/agents", label: "Agents", icon: Bot },
{ href: "/skills", label: "Skills", icon: Sparkles },
{ href: "/issues", label: "Issues", icon: ListTodo },
{ href: "/knowledge-base", label: "Knowledge Base", icon: BookOpen },
];
export function AppSidebar() {
const pathname = usePathname();
const router = useRouter();
const user = useAuthStore((s) => s.user);
const authLogout = useAuthStore((s) => s.logout);
const workspace = useWorkspaceStore((s) => s.workspace);
const workspaces = useWorkspaceStore((s) => s.workspaces);
const switchWorkspace = useWorkspaceStore((s) => s.switchWorkspace);
const unreadCount = useInboxStore((s) =>
s.items.filter((i) => !i.read && !i.archived).length
);
const logout = () => {
authLogout();
useWorkspaceStore.getState().clearWorkspace();
router.push("/login");
};
return (
<Sidebar variant="inset">
{/* Workspace Switcher */}
<SidebarHeader>
<div className="flex items-center gap-4">
<SidebarMenu className="min-w-0 flex-1">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton>
<WorkspaceAvatar name={workspace?.name ?? "M"} size="sm" />
<span className="flex-1 truncate font-medium">
{workspace?.name ?? "Multica"}
</span>
<ChevronDown className="size-3 text-muted-foreground" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
className="w-52"
align="start"
side="bottom"
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground">
{user?.email}
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
render={<Link href="/settings" />}
>
<Settings className="h-3.5 w-3.5" />
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="group/ws-section">
<DropdownMenuLabel className="flex items-center text-xs text-muted-foreground">
Workspaces
<Tooltip>
<TooltipTrigger
className="ml-auto opacity-0 group-hover/ws-section:opacity-100 transition-opacity rounded hover:bg-accent p-0.5"
onClick={() => useModalStore.getState().open("create-workspace")}
>
<Plus className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent side="right">
Create workspace
</TooltipContent>
</Tooltip>
</DropdownMenuLabel>
{workspaces.map((ws) => (
<DropdownMenuItem
key={ws.id}
onClick={() => {
if (ws.id !== workspace?.id) {
switchWorkspace(ws.id);
}
}}
>
<WorkspaceAvatar name={ws.name} size="sm" />
<span className="flex-1 truncate">{ws.name}</span>
{ws.id === workspace?.id && (
<Check className="h-3.5 w-3.5 text-primary" />
)}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive" onClick={logout}>
<LogOut className="h-3.5 w-3.5" />
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Search className="size-4" />
</TooltipTrigger>
<TooltipContent side="bottom">Search</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
className="flex h-7 w-7 items-center justify-center rounded-lg bg-background text-foreground shadow-sm hover:bg-accent"
onClick={() => useModalStore.getState().open("create-issue")}
>
<SquarePen className="size-3.5" />
</TooltipTrigger>
<TooltipContent side="bottom">New issue</TooltipContent>
</Tooltip>
</div>
</div>
</SidebarHeader>
{/* Navigation */}
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu className="gap-0.5">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
pathname.startsWith(item.href + "/");
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
isActive={isActive}
render={<Link href={item.href} />}
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
>
<item.icon />
<span>{item.label}</span>
{item.label === "Inbox" && unreadCount > 0 && (
<span className="ml-auto rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-medium text-primary-foreground">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{/* User */}
<SidebarFooter>
{user && (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="sm">
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-medium">
{user.name
.split(" ")
.map((w) => w[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
<span className="truncate">{user.name}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)}
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -1,12 +0,0 @@
export default function AgentDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
return (
<div className="p-6">
<h1 className="text-2xl font-bold">Agent Detail</h1>
<p className="mt-2 text-muted-foreground">Agent status and task history</p>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
export default function BoardPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold">Board</h1>
<p className="mt-2 text-muted-foreground">
Kanban board view coming soon.
</p>
</div>
);
}

View File

@@ -1,296 +1 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useInboxStore } from "@/features/inbox";
import { toast } from "sonner";
import {
AlertCircle,
Bot,
CheckCircle2,
CircleDot,
GitPullRequest,
MessageSquare,
ArrowRightLeft,
} from "lucide-react";
import type { InboxItem, InboxItemType, InboxSeverity } from "@multica/types";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/shared/api";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const severityOrder: Record<InboxSeverity, number> = {
action_required: 0,
attention: 1,
info: 2,
};
const typeIcons: Record<InboxItemType, typeof AlertCircle> = {
agent_blocked: AlertCircle,
review_requested: GitPullRequest,
issue_assigned: CircleDot,
agent_completed: CheckCircle2,
mentioned: MessageSquare,
status_change: ArrowRightLeft,
};
const severityColors: Record<InboxSeverity, string> = {
action_required: "text-destructive",
attention: "text-warning",
info: "text-muted-foreground",
};
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
// ---------------------------------------------------------------------------
// Components
// ---------------------------------------------------------------------------
function InboxListItem({
item,
isSelected,
onClick,
}: {
item: InboxItem;
isSelected: boolean;
onClick: () => void;
}) {
const Icon = typeIcons[item.type] ?? CircleDot;
const colorClass = severityColors[item.severity];
return (
<button
onClick={onClick}
className={`flex w-full items-start gap-3 px-4 py-3 text-left transition-colors ${
isSelected ? "bg-accent" : "hover:bg-accent/50"
} ${!item.read ? "font-medium" : ""}`}
>
<Icon className={`mt-0.5 h-4 w-4 shrink-0 ${colorClass}`} />
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<span className="truncate text-sm">{item.title}</span>
<span className="shrink-0 text-xs text-muted-foreground">
{timeAgo(item.created_at)}
</span>
</div>
{(item.type === "agent_blocked" || item.type === "review_requested") && (
<div className="mt-0.5 flex items-center gap-1.5">
<Bot className="h-3 w-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Agent action</span>
</div>
)}
</div>
{!item.read && (
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" />
)}
</button>
);
}
function InboxDetail({
item,
onMarkRead,
onArchive,
}: {
item: InboxItem;
onMarkRead: (id: string) => void;
onArchive: (id: string) => void;
}) {
const Icon = typeIcons[item.type] ?? CircleDot;
const colorClass = severityColors[item.severity];
const severityLabel: Record<InboxSeverity, string> = {
action_required: "Action required",
attention: "Needs attention",
info: "Info",
};
return (
<div className="p-6">
{/* Header */}
<div className="flex items-start gap-3">
<Icon className={`mt-1 h-5 w-5 shrink-0 ${colorClass}`} />
<div className="min-w-0 flex-1">
<h2 className="text-lg font-semibold truncate">{item.title}</h2>
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span className={colorClass}>{severityLabel[item.severity]}</span>
<span>·</span>
<span>{timeAgo(item.created_at)}</span>
</div>
</div>
{!item.read && (
<Button
variant="outline"
size="xs"
onClick={() => onMarkRead(item.id)}
className="shrink-0"
>
Mark read
</Button>
)}
{item.issue_id && (
<Link
href={`/issues/${item.issue_id}`}
className="inline-flex h-7 shrink-0 items-center rounded-md border px-2.5 text-xs font-medium transition-colors hover:bg-accent"
>
View Issue
</Link>
)}
<Button
variant="outline"
size="xs"
onClick={() => onArchive(item.id)}
className="shrink-0"
>
Archive
</Button>
</div>
{/* Body */}
{item.body && (
<div className="mt-6 whitespace-pre-wrap text-sm leading-relaxed text-foreground/80">
{item.body}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default function InboxPage() {
const [selectedId, setSelectedId] = useState<string>("");
// Read from global store (populated by workspace hydrate + useRealtimeSync)
const storeItems = useInboxStore((s) => s.items);
const loading = useInboxStore((s) => s.loading);
// Sort: severity first, then newest first
const items = useMemo(() => {
return [...storeItems]
.filter((i) => !i.archived)
.sort(
(a, b) =>
severityOrder[a.severity] - severityOrder[b.severity] ||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
}, [storeItems]);
// Auto-select first item when items change
useEffect(() => {
if (items.length > 0 && !selectedId) {
setSelectedId(items[0]!.id);
}
}, [items, selectedId]);
const handleMarkRead = async (id: string) => {
try {
await api.markInboxRead(id);
useInboxStore.getState().markRead(id);
} catch (err) {
toast.error("Failed to mark as read");
}
};
const handleArchive = async (id: string) => {
try {
await api.archiveInbox(id);
useInboxStore.getState().archive(id);
// If archived item was selected, clear selection
if (selectedId === id) {
setSelectedId("");
}
} catch (err) {
toast.error("Failed to archive");
}
};
const selected = items.find((i) => i.id === selectedId) ?? null;
const unreadCount = items.filter((i) => !i.read).length;
if (loading) {
return (
<div className="flex flex-1 min-h-0">
<div className="w-80 shrink-0 border-r">
<div className="flex h-12 items-center border-b px-4">
<Skeleton className="h-5 w-16" />
</div>
<div className="space-y-1 p-2">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-start gap-3 px-4 py-3">
<Skeleton className="h-4 w-4 shrink-0 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
))}
</div>
</div>
<div className="flex-1 p-6">
<Skeleton className="h-6 w-48" />
<Skeleton className="mt-4 h-4 w-32" />
<Skeleton className="mt-6 h-24 w-full" />
</div>
</div>
);
}
return (
<div className="flex flex-1 min-h-0">
{/* Left column — inbox list */}
<div className="w-80 shrink-0 overflow-y-auto border-r">
<div className="flex h-12 items-center border-b px-4">
<h1 className="text-sm font-semibold">Inbox</h1>
{unreadCount > 0 && (
<span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
{unreadCount}
</span>
)}
</div>
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-sm text-muted-foreground">
<p>No notifications yet</p>
</div>
) : (
<div className="divide-y">
{items.map((item) => (
<InboxListItem
key={item.id}
item={item}
isSelected={item.id === selectedId}
onClick={() => setSelectedId(item.id)}
/>
))}
</div>
)}
</div>
{/* Right column — detail */}
<div className="flex-1 overflow-y-auto">
{selected ? (
<InboxDetail item={selected} onMarkRead={handleMarkRead} onArchive={handleArchive} />
) : (
<div className="flex h-full items-center justify-center text-muted-foreground">
{items.length === 0
? "Your inbox is empty"
: "Select an item to view details"}
</div>
)}
</div>
</div>
);
}
export { InboxPage as default } from "@multica/views/inbox";

View File

@@ -1,274 +0,0 @@
import { Suspense } from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { Issue, Comment } from "@multica/types";
// Mock next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => "/issues/issue-1",
}));
// Mock next/link
vi.mock("next/link", () => ({
default: ({
children,
href,
...props
}: {
children: React.ReactNode;
href: string;
[key: string]: any;
}) => (
<a href={href} {...props}>
{children}
</a>
),
}));
// Mock auth store
vi.mock("@/features/auth", () => ({
useAuthStore: (selector: (s: any) => any) =>
selector({
user: { id: "user-1", name: "Test User", email: "test@multica.ai" },
isLoading: false,
}),
}));
// Mock workspace feature
vi.mock("@/features/workspace", () => ({
useWorkspaceStore: (selector: (s: any) => any) =>
selector({
workspace: { id: "ws-1", name: "Test WS" },
workspaces: [{ id: "ws-1", name: "Test WS" }],
members: [{ user_id: "user-1", name: "Test User", email: "test@multica.ai" }],
agents: [{ id: "agent-1", name: "Claude Agent" }],
}),
useActorName: () => ({
getMemberName: (id: string) => (id === "user-1" ? "Test User" : "Unknown"),
getAgentName: (id: string) => (id === "agent-1" ? "Claude Agent" : "Unknown Agent"),
getActorName: (type: string, id: string) => {
if (type === "member" && id === "user-1") return "Test User";
if (type === "agent" && id === "agent-1") return "Claude Agent";
return "Unknown";
},
getActorInitials: (type: string, id: string) => {
if (type === "member") return "TU";
if (type === "agent") return "CA";
return "??";
},
}),
}));
// Mock ws-context
vi.mock("@/features/realtime", () => ({
useWSEvent: () => {},
}));
// Mock calendar (react-day-picker needs browser APIs)
vi.mock("@/components/ui/calendar", () => ({
Calendar: () => null,
}));
// Mock api
const mockGetIssue = vi.hoisted(() => vi.fn());
const mockListComments = vi.hoisted(() => vi.fn());
const mockCreateComment = vi.hoisted(() => vi.fn());
const mockUpdateComment = vi.hoisted(() => vi.fn());
const mockDeleteComment = vi.hoisted(() => vi.fn());
const mockDeleteIssue = vi.hoisted(() => vi.fn());
const mockUpdateIssue = vi.hoisted(() => vi.fn());
vi.mock("@/shared/api", () => ({
api: {
getIssue: (...args: any[]) => mockGetIssue(...args),
listComments: (...args: any[]) => mockListComments(...args),
createComment: (...args: any[]) => mockCreateComment(...args),
updateComment: (...args: any[]) => mockUpdateComment(...args),
deleteComment: (...args: any[]) => mockDeleteComment(...args),
deleteIssue: (...args: any[]) => mockDeleteIssue(...args),
updateIssue: (...args: any[]) => mockUpdateIssue(...args),
},
}));
const mockIssue: Issue = {
id: "issue-1",
workspace_id: "ws-1",
title: "Implement authentication",
description: "Add JWT auth to the backend",
status: "in_progress",
priority: "high",
assignee_type: "member",
assignee_id: "user-1",
creator_type: "member",
creator_id: "user-1",
parent_issue_id: null,
acceptance_criteria: [],
context_refs: [],
position: 0,
due_date: "2026-06-01T00:00:00Z",
created_at: "2026-01-15T00:00:00Z",
updated_at: "2026-01-20T00:00:00Z",
};
const mockComments: Comment[] = [
{
id: "comment-1",
issue_id: "issue-1",
content: "Started working on this",
type: "comment",
author_type: "member",
author_id: "user-1",
created_at: "2026-01-16T00:00:00Z",
updated_at: "2026-01-16T00:00:00Z",
},
{
id: "comment-2",
issue_id: "issue-1",
content: "I can help with this",
type: "comment",
author_type: "agent",
author_id: "agent-1",
created_at: "2026-01-17T00:00:00Z",
updated_at: "2026-01-17T00:00:00Z",
},
];
import IssueDetailPage from "./page";
// React 19 use(Promise) needs the promise to resolve within act + Suspense
async function renderPage(id = "issue-1") {
let result: ReturnType<typeof render>;
await act(async () => {
result = render(
<Suspense fallback={<div>Suspense loading...</div>}>
<IssueDetailPage params={Promise.resolve({ id })} />
</Suspense>,
);
});
return result!;
}
describe("IssueDetailPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders issue details after loading", async () => {
mockGetIssue.mockResolvedValueOnce(mockIssue);
mockListComments.mockResolvedValueOnce(mockComments);
await renderPage();
await waitFor(() => {
expect(
screen.getByText("Implement authentication"),
).toBeInTheDocument();
});
expect(
screen.getByText("Add JWT auth to the backend"),
).toBeInTheDocument();
});
it("renders issue properties sidebar", async () => {
mockGetIssue.mockResolvedValueOnce(mockIssue);
mockListComments.mockResolvedValueOnce(mockComments);
await renderPage();
await waitFor(() => {
expect(screen.getByText("Properties")).toBeInTheDocument();
});
expect(screen.getByText("In Progress")).toBeInTheDocument();
expect(screen.getByText("High")).toBeInTheDocument();
});
it("renders comments", async () => {
mockGetIssue.mockResolvedValueOnce(mockIssue);
mockListComments.mockResolvedValueOnce(mockComments);
await renderPage();
await waitFor(() => {
expect(
screen.getByText("Started working on this"),
).toBeInTheDocument();
});
expect(screen.getByText("I can help with this")).toBeInTheDocument();
expect(screen.getByText("Activity")).toBeInTheDocument();
});
it("shows 'Issue not found' for missing issue", async () => {
mockGetIssue.mockRejectedValueOnce(new Error("Not found"));
mockListComments.mockRejectedValueOnce(new Error("Not found"));
await renderPage("nonexistent-id");
await waitFor(() => {
expect(screen.getByText("Issue not found")).toBeInTheDocument();
});
});
it("submits a new comment", async () => {
mockGetIssue.mockResolvedValueOnce(mockIssue);
mockListComments.mockResolvedValueOnce(mockComments);
const newComment: Comment = {
id: "comment-3",
issue_id: "issue-1",
content: "New test comment",
type: "comment",
author_type: "member",
author_id: "user-1",
created_at: "2026-01-18T00:00:00Z",
updated_at: "2026-01-18T00:00:00Z",
};
mockCreateComment.mockResolvedValueOnce(newComment);
const user = userEvent.setup();
await renderPage();
await waitFor(() => {
expect(
screen.getByPlaceholderText("Leave a comment..."),
).toBeInTheDocument();
});
await user.type(
screen.getByPlaceholderText("Leave a comment..."),
"New test comment",
);
const form = screen
.getByPlaceholderText("Leave a comment...")
.closest("form")!;
const submitBtn = form.querySelector(
'button[type="submit"]',
) as HTMLElement;
await user.click(submitBtn);
await waitFor(() => {
expect(mockCreateComment).toHaveBeenCalledWith(
"issue-1",
"New test comment",
);
});
await waitFor(() => {
expect(screen.getByText("New test comment")).toBeInTheDocument();
});
});
it("renders breadcrumb navigation", async () => {
mockGetIssue.mockResolvedValueOnce(mockIssue);
mockListComments.mockResolvedValueOnce(mockComments);
await renderPage();
await waitFor(() => {
expect(screen.getByText("Issues")).toBeInTheDocument();
});
const issuesLink = screen.getByText("Issues");
expect(issuesLink.closest("a")).toHaveAttribute("href", "/issues");
});
});

View File

@@ -1,312 +1,7 @@
"use client";
import { use, useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
ChevronRight,
Link2,
Pencil,
Send,
Trash2,
X,
} from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { ActorAvatar } from "@/components/common/actor-avatar";
import type { Issue, Comment, UpdateIssueRequest } from "@multica/types";
import { StatusPicker, PriorityPicker, AssigneePicker } from "@/features/issues/components";
import { api } from "@/shared/api";
import { useAuthStore } from "@/features/auth";
import { useActorName } from "@/features/workspace";
import { useWSEvent } from "@/features/realtime";
import { useIssueStore } from "@/features/issues";
import type { CommentCreatedPayload, CommentUpdatedPayload, CommentDeletedPayload } from "@multica/types";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function shortDate(date: string | null): string {
if (!date) return "—";
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
// ---------------------------------------------------------------------------
// Property row
// ---------------------------------------------------------------------------
function PropRow({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div className="flex min-h-[32px] items-center gap-3 rounded-md px-2 -mx-2 hover:bg-accent/50 transition-colors">
<span className="w-20 shrink-0 text-[13px] text-muted-foreground">{label}</span>
<div className="flex min-w-0 flex-1 items-center justify-end gap-1.5 text-[13px]">
{children}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Due Date Picker
// ---------------------------------------------------------------------------
function DueDatePicker({
dueDate,
onUpdate,
}: {
dueDate: string | null;
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
}) {
const [open, setOpen] = useState(false);
const date = dueDate ? new Date(dueDate) : undefined;
const isOverdue = date ? date < new Date() : false;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors">
{date ? (
<span className={isOverdue ? "text-destructive" : ""}>
{date.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
</span>
) : (
<span className="text-muted-foreground">None</span>
)}
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
mode="single"
selected={date}
onSelect={(d: Date | undefined) => {
onUpdate({ due_date: d ? d.toISOString() : null });
setOpen(false);
}}
/>
{date && (
<div className="border-t px-3 py-2">
<Button
variant="ghost"
size="xs"
onClick={() => {
onUpdate({ due_date: null });
setOpen(false);
}}
className="text-muted-foreground hover:text-foreground"
>
Clear date
</Button>
</div>
)}
</PopoverContent>
</Popover>
);
}
// ---------------------------------------------------------------------------
// Acceptance Criteria Editor
// ---------------------------------------------------------------------------
function AcceptanceCriteriaEditor({
criteria,
onUpdate,
}: {
criteria: string[];
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
}) {
const [newItem, setNewItem] = useState("");
const addItem = () => {
if (!newItem.trim()) return;
onUpdate({ acceptance_criteria: [...criteria, newItem.trim()] });
setNewItem("");
};
const removeItem = (index: number) => {
onUpdate({ acceptance_criteria: criteria.filter((_, i) => i !== index) });
};
const [adding, setAdding] = useState(false);
return (
<div className="space-y-2">
<h3 className="text-xs font-medium text-muted-foreground">Acceptance Criteria</h3>
{criteria.length > 0 && (
<div className="space-y-1">
{criteria.map((item, i) => (
<div key={i} className="group flex items-start gap-2 text-sm">
<span className="mt-0.5 text-muted-foreground">&bull;</span>
<span className="flex-1">{item}</span>
<Button
variant="ghost"
size="icon-xs"
onClick={() => removeItem(i)}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground transition-opacity"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
))}
</div>
)}
{(criteria.length > 0 || adding) ? (
<form
onSubmit={(e) => { e.preventDefault(); addItem(); }}
className="flex items-center gap-2"
>
<input
autoFocus={adding}
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
onBlur={() => { if (!newItem.trim()) setAdding(false); }}
placeholder="Add criteria..."
aria-label="Add acceptance criteria"
className="flex-1 text-sm bg-transparent outline-none placeholder:text-muted-foreground"
/>
</form>
) : (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground h-7 px-2 text-xs"
onClick={() => setAdding(true)}
>
+ Add acceptance criteria
</Button>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Context Refs Editor
// ---------------------------------------------------------------------------
function ContextRefsEditor({
refs,
onUpdate,
}: {
refs: string[];
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
}) {
const [newRef, setNewRef] = useState("");
const addRef = () => {
if (!newRef.trim()) return;
onUpdate({ context_refs: [...refs, newRef.trim()] });
setNewRef("");
};
const removeRef = (index: number) => {
onUpdate({ context_refs: refs.filter((_, i) => i !== index) });
};
const [adding, setAdding] = useState(false);
const isUrl = (s: string) => s.startsWith("http://") || s.startsWith("https://");
return (
<div className="space-y-2">
<h3 className="text-xs font-medium text-muted-foreground">Context References</h3>
{refs.length > 0 && (
<div className="space-y-1">
{refs.map((ref, i) => (
<div key={i} className="group flex items-center gap-2 text-sm">
<Link2 className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
{isUrl(ref) ? (
<a href={ref} target="_blank" rel="noopener noreferrer" className="flex-1 text-info hover:underline truncate">
{ref}
</a>
) : (
<span className="flex-1 truncate">{ref}</span>
)}
<Button
variant="ghost"
size="icon-xs"
onClick={() => removeRef(i)}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground transition-opacity"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
))}
</div>
)}
{(refs.length > 0 || adding) ? (
<form
onSubmit={(e) => { e.preventDefault(); addRef(); }}
className="flex items-center gap-2"
>
<input
autoFocus={adding}
value={newRef}
onChange={(e) => setNewRef(e.target.value)}
onBlur={() => { if (!newRef.trim()) setAdding(false); }}
placeholder="Add reference URL..."
aria-label="Add context reference URL"
className="flex-1 text-sm bg-transparent outline-none placeholder:text-muted-foreground"
/>
</form>
) : (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground h-7 px-2 text-xs"
onClick={() => setAdding(true)}
>
+ Add context reference
</Button>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
import { use } from "react";
import { IssueDetail } from "@multica/views/issues/components";
export default function IssueDetailPage({
params,
@@ -314,437 +9,5 @@ export default function IssueDetailPage({
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const router = useRouter();
const user = useAuthStore((s) => s.user);
const { getActorName, getActorInitials } = useActorName();
const [issue, setIssue] = useState<Issue | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState(true);
const [commentText, setCommentText] = useState("");
const [submitting, setSubmitting] = useState(false);
const [deleting, setDeleting] = useState(false);
const [editingCommentId, setEditingCommentId] = useState<string | null>(null);
const [editContent, setEditContent] = useState("");
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const [editingDesc, setEditingDesc] = useState(false);
const [descDraft, setDescDraft] = useState("");
// Watch the global issue store for real-time updates from other users/agents
const storeIssue = useIssueStore((s) => s.issues.find((i) => i.id === id));
useEffect(() => {
if (storeIssue) {
setIssue(storeIssue);
}
}, [storeIssue]);
useEffect(() => {
setIssue(null);
setComments([]);
setLoading(true);
Promise.all([api.getIssue(id), api.listComments(id)])
.then(([iss, cmts]) => {
setIssue(iss);
setComments(cmts);
})
.catch(console.error)
.finally(() => setLoading(false));
}, [id]);
const handleSubmitComment = async (e: React.FormEvent) => {
e.preventDefault();
if (!commentText.trim() || submitting || !user) return;
const content = commentText.trim();
const tempId = "temp-" + Date.now();
const tempComment: Comment = {
id: tempId,
issue_id: id,
author_type: "member",
author_id: user.id,
content,
type: "comment",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
setComments((prev) => [...prev, tempComment]);
setCommentText("");
setSubmitting(true);
try {
const comment = await api.createComment(id, content);
setComments((prev) => prev.map((c) => (c.id === tempId ? comment : c)));
} catch {
setComments((prev) => prev.filter((c) => c.id !== tempId));
toast.error("Failed to send comment");
} finally {
setSubmitting(false);
}
};
const handleUpdateField = useCallback(
(updates: Partial<UpdateIssueRequest>) => {
if (!issue) return;
const prev = issue;
setIssue((curr) => (curr ? ({ ...curr, ...updates } as Issue) : curr));
api.updateIssue(id, updates).catch(() => {
setIssue(prev);
toast.error("Failed to update issue");
});
},
[issue, id],
);
const handleDelete = async () => {
setDeleting(true);
try {
await api.deleteIssue(issue!.id);
toast.success("Issue deleted");
router.push("/issues");
} catch {
toast.error("Failed to delete issue");
setDeleting(false);
}
};
const startEditComment = (c: Comment) => {
setEditingCommentId(c.id);
setEditContent(c.content);
};
const handleSaveEditComment = async () => {
if (!editingCommentId || !editContent.trim()) return;
try {
const updated = await api.updateComment(editingCommentId, editContent.trim());
setComments((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
setEditingCommentId(null);
} catch {
toast.error("Failed to update comment");
}
};
const handleDeleteComment = async (commentId: string) => {
try {
await api.deleteComment(commentId);
setComments((prev) => prev.filter((c) => c.id !== commentId));
} catch {
toast.error("Failed to delete comment");
}
};
// Real-time comment updates
useWSEvent(
"comment:created",
useCallback((payload: unknown) => {
const { comment } = payload as CommentCreatedPayload;
if (comment.issue_id !== id) return;
// Skip own comments — already added locally via API response
if (comment.author_type === "member" && comment.author_id === user?.id) return;
setComments((prev) => {
if (prev.some((c) => c.id === comment.id)) return prev;
return [...prev, comment];
});
}, [id, user?.id]),
);
useWSEvent(
"comment:updated",
useCallback((payload: unknown) => {
const { comment } = payload as CommentUpdatedPayload;
if (comment.issue_id === id) {
setComments((prev) => prev.map((c) => (c.id === comment.id ? comment : c)));
}
}, [id]),
);
useWSEvent(
"comment:deleted",
useCallback((payload: unknown) => {
const { comment_id, issue_id } = payload as CommentDeletedPayload;
if (issue_id === id) {
setComments((prev) => prev.filter((c) => c.id !== comment_id));
}
}, [id]),
);
if (loading) {
return (
<div className="flex flex-1 min-h-0 items-center justify-center text-sm text-muted-foreground">
Loading...
</div>
);
}
if (!issue) {
return (
<div className="flex flex-1 min-h-0 items-center justify-center text-sm text-muted-foreground">
Issue not found
</div>
);
}
return (
<div className="flex flex-1 min-h-0">
{/* LEFT: Content area */}
<div className="flex-1 overflow-y-auto">
{/* Header bar */}
<div className="sticky top-0 z-10 flex h-11 items-center justify-between border-b bg-background px-6 text-[13px]">
<div className="flex items-center gap-1.5">
<Link
href="/issues"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Issues
</Link>
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
<span className="truncate text-muted-foreground">{issue.id.slice(0, 8)}</span>
</div>
<AlertDialog>
<AlertDialogTrigger
render={<Button variant="ghost" size="icon-xs" className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive" />}
>
<Trash2 className="h-4 w-4" />
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete issue</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this issue and all its comments. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={deleting}
className="bg-destructive text-white hover:bg-destructive/90"
>
{deleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{/* Content */}
<div className="mx-auto w-full max-w-3xl px-8 py-8">
<div className="mb-1 text-[13px] text-muted-foreground">{issue.id.slice(0, 8)}</div>
{editingTitle ? (
<Input
autoFocus
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onBlur={() => {
if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
setEditingTitle(false);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
setEditingTitle(false);
} else if (e.key === "Escape") {
setEditingTitle(false);
}
}}
className="text-xl font-semibold leading-snug tracking-tight"
/>
) : (
<h1
className="text-xl font-semibold leading-snug tracking-tight cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1"
onClick={() => { setTitleDraft(issue.title); setEditingTitle(true); }}
>
{issue.title}
</h1>
)}
{editingDesc ? (
<Textarea
autoFocus
value={descDraft}
onChange={(e) => setDescDraft(e.target.value)}
onBlur={() => {
handleUpdateField({ description: descDraft.trim() || undefined });
setEditingDesc(false);
}}
onKeyDown={(e) => {
if (e.key === "Escape") setEditingDesc(false);
}}
rows={4}
className="mt-5 text-[14px] leading-[1.7] resize-none"
/>
) : (
<div
className="mt-5 text-[14px] leading-[1.7] whitespace-pre-wrap cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1"
onClick={() => { setDescDraft(issue.description || ""); setEditingDesc(true); }}
>
{issue.description ? (
<span className="text-foreground/85">{issue.description}</span>
) : (
<span className="text-muted-foreground">Add description...</span>
)}
</div>
)}
<div className="space-y-4 mt-4">
<AcceptanceCriteriaEditor
criteria={issue.acceptance_criteria}
onUpdate={handleUpdateField}
/>
<ContextRefsEditor
refs={issue.context_refs}
onUpdate={handleUpdateField}
/>
</div>
<div className="my-8 border-t" />
{/* Activity / Comments */}
<div>
<h2 className="text-[13px] font-medium">Activity</h2>
<div className="mt-4">
{comments.map((comment) => {
const isOwn = comment.author_type === "member" && comment.author_id === user?.id;
return (
<div key={comment.id} className={`group relative py-3${comment.id.startsWith("temp-") ? " opacity-60" : ""}`}>
<div className="flex items-center gap-2.5">
<ActorAvatar
actorType={comment.author_type}
actorId={comment.author_id}
size={28}
getName={getActorName}
getInitials={getActorInitials}
/>
<span className="text-[13px] font-medium">
{getActorName(comment.author_type, comment.author_id)}
</span>
<Tooltip>
<TooltipTrigger
render={
<span className="text-[12px] text-muted-foreground cursor-default">
{timeAgo(comment.created_at)}
</span>
}
/>
<TooltipContent side="top">
{new Date(comment.created_at).toLocaleString()}
</TooltipContent>
</Tooltip>
{isOwn && (
<div className="ml-auto flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon-xs"
onClick={() => startEditComment(comment)}
className="text-muted-foreground hover:text-foreground"
>
<Pencil className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() => handleDeleteComment(comment.id)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
)}
</div>
{editingCommentId === comment.id ? (
<form onSubmit={(e) => { e.preventDefault(); handleSaveEditComment(); }} className="mt-2 pl-[38px]">
<input
autoFocus
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
aria-label="Edit comment"
className="w-full text-[13px] bg-transparent border-b outline-none"
onKeyDown={(e) => { if (e.key === "Escape") setEditingCommentId(null); }}
/>
</form>
) : (
<div className="mt-2 pl-[38px] text-[13px] leading-[1.6] text-foreground/85 whitespace-pre-wrap">
{comment.content}
</div>
)}
</div>
);
})}
</div>
{/* Comment input */}
<form onSubmit={handleSubmitComment} className="mt-2 border-t pt-4">
<div className="flex items-center gap-2">
<Input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Leave a comment..."
className="flex-1 text-[13px]"
/>
<Button
type="submit"
size="icon"
disabled={!commentText.trim() || submitting}
>
<Send className="h-3.5 w-3.5" />
</Button>
</div>
</form>
</div>
</div>
</div>
{/* RIGHT: Properties sidebar */}
<div className="w-60 shrink-0 overflow-y-auto border-l">
<div className="p-4">
<div className="mb-2 text-[12px] font-medium text-muted-foreground">
Properties
</div>
<div className="space-y-0.5">
<PropRow label="Status">
<StatusPicker status={issue.status} onUpdate={handleUpdateField} />
</PropRow>
<PropRow label="Priority">
<PriorityPicker priority={issue.priority} onUpdate={handleUpdateField} />
</PropRow>
<PropRow label="Assignee">
<AssigneePicker
assigneeType={issue.assignee_type}
assigneeId={issue.assignee_id}
onUpdate={handleUpdateField}
/>
</PropRow>
<PropRow label="Due date">
<DueDatePicker dueDate={issue.due_date} onUpdate={handleUpdateField} />
</PropRow>
<PropRow label="Created by">
<ActorAvatar
actorType={issue.creator_type}
actorId={issue.creator_id}
size={18}
getName={getActorName}
getInitials={getActorInitials}
/>
<span>{getActorName(issue.creator_type, issue.creator_id)}</span>
</PropRow>
</div>
<div className="mt-4 border-t pt-3 space-y-0.5">
<PropRow label="Created">
<span className="text-muted-foreground">{shortDate(issue.created_at)}</span>
</PropRow>
<PropRow label="Updated">
<span className="text-muted-foreground">{shortDate(issue.updated_at)}</span>
</PropRow>
</div>
</div>
</div>
</div>
);
return <IssueDetail issueId={id} />;
}

View File

@@ -1,287 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { Issue } from "@multica/types";
// Mock next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => "/issues",
}));
// Mock next/link
vi.mock("next/link", () => ({
default: ({
children,
href,
...props
}: {
children: React.ReactNode;
href: string;
[key: string]: any;
}) => (
<a href={href} {...props}>
{children}
</a>
),
}));
// Mock workspace feature
vi.mock("@/features/workspace", () => ({
useActorName: () => ({
getMemberName: (id: string) => (id === "user-1" ? "Test User" : "Unknown"),
getAgentName: (id: string) => (id === "agent-1" ? "Claude Agent" : "Unknown Agent"),
getActorName: (type: string, id: string) =>
type === "member" ? "Test User" : "Claude Agent",
getActorInitials: () => "TU",
}),
useWorkspaceStore: Object.assign(
(selector?: any) => {
const state = { workspace: { id: "ws-1", name: "Test", slug: "test" }, agents: [], members: [] };
return selector ? selector(state) : state;
},
{ getState: () => ({ workspace: { id: "ws-1", name: "Test", slug: "test" }, agents: [], members: [] }) },
),
}));
// Mock WebSocket context
vi.mock("@/features/realtime", () => ({
useWSEvent: vi.fn(),
useWS: () => ({ subscribe: vi.fn(() => () => {}) }),
WSProvider: ({ children }: { children: React.ReactNode }) => children,
}));
// Mock sonner toast
vi.mock("sonner", () => ({
toast: { error: vi.fn(), success: vi.fn() },
}));
// Mock api
const mockCreateIssue = vi.fn();
const mockUpdateIssue = vi.fn();
vi.mock("@/shared/api", () => ({
api: {
listIssues: vi.fn().mockResolvedValue({ issues: [], total: 0 }),
createIssue: (...args: any[]) => mockCreateIssue(...args),
updateIssue: (...args: any[]) => mockUpdateIssue(...args),
},
}));
// Mock the issue store — control state directly
let mockStoreState: {
issues: Issue[];
loading: boolean;
fetch: () => Promise<void>;
setIssues: (issues: Issue[]) => void;
addIssue: (issue: Issue) => void;
updateIssue: (id: string, updates: Partial<Issue>) => void;
removeIssue: (id: string) => void;
};
vi.mock("@/features/issues", () => ({
useIssueStore: (selector?: any) => {
return selector ? selector(mockStoreState) : mockStoreState;
},
StatusIcon: () => null,
StatusPicker: ({ value, onChange }: any) => (
<button onClick={() => onChange?.("todo")}>{value || "todo"}</button>
),
PriorityPicker: ({ value, onChange }: any) => (
<button onClick={() => onChange?.("none")}>{value || "none"}</button>
),
statusConfig: {
backlog: { label: "Backlog" },
todo: { label: "Todo" },
in_progress: { label: "In Progress" },
in_review: { label: "In Review" },
done: { label: "Done" },
blocked: { label: "Blocked" },
cancelled: { label: "Cancelled" },
},
priorityConfig: {
urgent: { label: "Urgent" },
high: { label: "High" },
medium: { label: "Medium" },
low: { label: "Low" },
none: { label: "None" },
},
}));
// Mock modals
vi.mock("@/features/modals", () => ({
useModalStore: Object.assign(
() => ({ open: vi.fn() }),
{ getState: () => ({ open: vi.fn() }) },
),
}));
const issueDefaults = {
parent_issue_id: null,
acceptance_criteria: [],
context_refs: [],
position: 0,
};
const mockIssues: Issue[] = [
{
...issueDefaults,
id: "issue-1",
workspace_id: "ws-1",
title: "Implement auth",
description: "Add JWT authentication",
status: "todo",
priority: "high",
assignee_type: "member",
assignee_id: "user-1",
creator_type: "member",
creator_id: "user-1",
due_date: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
},
{
...issueDefaults,
id: "issue-2",
workspace_id: "ws-1",
title: "Design landing page",
description: null,
status: "in_progress",
priority: "medium",
assignee_type: "agent",
assignee_id: "agent-1",
creator_type: "member",
creator_id: "user-1",
due_date: "2026-02-01T00:00:00Z",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
},
{
...issueDefaults,
id: "issue-3",
workspace_id: "ws-1",
title: "Write tests",
description: null,
status: "backlog",
priority: "low",
assignee_type: null,
assignee_id: null,
creator_type: "member",
creator_id: "user-1",
due_date: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
},
];
import IssuesPage from "./page";
describe("IssuesPage", () => {
beforeEach(() => {
vi.clearAllMocks();
mockStoreState = {
issues: [],
loading: true,
fetch: vi.fn(),
setIssues: vi.fn(),
addIssue: vi.fn(),
updateIssue: vi.fn(),
removeIssue: vi.fn(),
};
});
it("shows loading state initially", () => {
mockStoreState.loading = true;
mockStoreState.issues = [];
render(<IssuesPage />);
// Now shows skeleton instead of text
expect(screen.getAllByRole("generic").some(el => el.getAttribute("data-slot") === "skeleton")).toBe(true);
});
it("renders issues in board view after loading", async () => {
mockStoreState.loading = false;
mockStoreState.issues = mockIssues;
render(<IssuesPage />);
expect(screen.getByText("Implement auth")).toBeInTheDocument();
expect(screen.getByText("Design landing page")).toBeInTheDocument();
expect(screen.getByText("Write tests")).toBeInTheDocument();
});
it("renders board columns", async () => {
mockStoreState.loading = false;
mockStoreState.issues = mockIssues;
render(<IssuesPage />);
expect(screen.getAllByText("Backlog").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Todo").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Progress").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Review").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Done").length).toBeGreaterThanOrEqual(1);
});
it("switches to list view", async () => {
mockStoreState.loading = false;
mockStoreState.issues = mockIssues;
const user = userEvent.setup();
render(<IssuesPage />);
expect(screen.getByText("Implement auth")).toBeInTheDocument();
const listButton = screen.getByText("List");
await user.click(listButton);
expect(screen.getByText("Implement auth")).toBeInTheDocument();
expect(screen.getByText("Design landing page")).toBeInTheDocument();
});
it("shows 'New Issue' button", async () => {
mockStoreState.loading = false;
mockStoreState.issues = [];
render(<IssuesPage />);
expect(screen.getByText("New Issue")).toBeInTheDocument();
});
it("shows create dialog when New Issue is clicked", async () => {
mockStoreState.loading = false;
mockStoreState.issues = [];
const user = userEvent.setup();
render(<IssuesPage />);
expect(screen.getByText("New Issue")).toBeInTheDocument();
await user.click(screen.getByText("New Issue"));
// Create dialog is now a global modal, just check the button was clicked
// The modal renders in ModalRegistry which is outside IssuesPage
});
it("creates an issue via the dialog", async () => {
mockStoreState.loading = false;
mockStoreState.issues = [];
const user = userEvent.setup();
render(<IssuesPage />);
expect(screen.getByText("New Issue")).toBeInTheDocument();
await user.click(screen.getByText("New Issue"));
// Create dialog is now a global modal in ModalRegistry
// This test verifies the page itself doesn't crash
});
it("handles API error gracefully", async () => {
mockStoreState.loading = false;
mockStoreState.issues = [];
render(<IssuesPage />);
// Should render without crashing even with empty issues
expect(screen.queryAllByRole("generic").length).toBeGreaterThan(0);
});
});

View File

@@ -1,471 +1,7 @@
"use client";
import { useState, useCallback, useMemo } from "react";
import { useIssueStore } from "@/features/issues";
import { useModalStore } from "@/features/modals";
import { toast } from "sonner";
import Link from "next/link";
import {
Columns3,
List,
Plus,
} from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import {
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
useDroppable,
closestCorners,
type DragStartEvent,
type DragEndEvent,
} from "@dnd-kit/core";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { Issue, IssueStatus, IssuePriority } from "@multica/types";
import { STATUS_CONFIG, PRIORITY_CONFIG, ALL_STATUSES, PRIORITY_ORDER, STATUS_ORDER } from "@/features/issues/config";
import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectGroup,
} from "@/components/ui/select";
import { ActorAvatar } from "@/components/common/actor-avatar";
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
import { api } from "@/shared/api";
import { useActorName } from "@/features/workspace";
import { IssuesPage } from "@multica/views/issues/components";
function formatDate(date: string): string {
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
const BOARD_STATUSES: IssueStatus[] = [
"backlog",
"todo",
"in_progress",
"in_review",
"done",
"blocked",
];
// ---------------------------------------------------------------------------
// Board View — Card
// ---------------------------------------------------------------------------
function BoardCardContent({ issue }: { issue: Issue }) {
const { getActorName, getActorInitials } = useActorName();
return (
<div className="rounded-lg border bg-background p-3">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<PriorityIcon priority={issue.priority} />
<span>{issue.id.slice(0, 8)}</span>
</div>
<p className="mt-1.5 text-[13px] leading-snug">{issue.title}</p>
<div className="mt-2.5 flex items-center justify-between">
<div className="flex items-center gap-2">
{issue.assignee_type && issue.assignee_id && (
<ActorAvatar
actorType={issue.assignee_type}
actorId={issue.assignee_id}
size={20}
getName={getActorName}
getInitials={getActorInitials}
/>
)}
</div>
{issue.due_date && (
<span className="text-xs text-muted-foreground">
{formatDate(issue.due_date)}
</span>
)}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Draggable card wrapper
// ---------------------------------------------------------------------------
function DraggableBoardCard({ issue }: { issue: Issue }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: issue.id,
data: { status: issue.status },
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={isDragging ? "opacity-30" : ""}
>
<Link
href={`/issues/${issue.id}`}
className={`block transition-colors hover:opacity-80 ${isDragging ? "pointer-events-none" : ""}`}
>
<BoardCardContent issue={issue} />
</Link>
</div>
);
}
// ---------------------------------------------------------------------------
// Droppable column
// ---------------------------------------------------------------------------
function DroppableColumn({
status,
issues,
}: {
status: IssueStatus;
issues: Issue[];
}) {
const cfg = STATUS_CONFIG[status];
const { setNodeRef, isOver } = useDroppable({ id: status });
return (
<div className="flex min-w-52 flex-1 flex-col">
<div className="mb-2 flex items-center gap-2 px-1">
<StatusIcon status={status} className="h-3.5 w-3.5" />
<span className="text-xs font-medium">{cfg.label}</span>
<span className="text-xs text-muted-foreground">{issues.length}</span>
</div>
<div
ref={setNodeRef}
className={`min-h-[200px] flex-1 space-y-1.5 overflow-y-auto rounded-lg p-1 transition-colors ${
isOver ? "bg-accent/40" : ""
}`}
>
{issues.map((issue) => (
<DraggableBoardCard key={issue.id} issue={issue} />
))}
{issues.length === 0 && (
<p className="py-8 text-center text-xs text-muted-foreground">No issues</p>
)}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Board View (with DnD)
// ---------------------------------------------------------------------------
function BoardView({
issues,
onMoveIssue,
}: {
issues: Issue[];
onMoveIssue: (issueId: string, newStatus: IssueStatus) => void;
}) {
const [activeIssue, setActiveIssue] = useState<Issue | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
})
);
const visibleStatuses = BOARD_STATUSES;
const handleDragStart = useCallback(
(event: DragStartEvent) => {
const issue = issues.find((i) => i.id === event.active.id);
if (issue) setActiveIssue(issue);
},
[issues]
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
setActiveIssue(null);
const { active, over } = event;
if (!over) return;
const issueId = active.id as string;
let targetStatus: IssueStatus | undefined;
if (visibleStatuses.includes(over.id as IssueStatus)) {
targetStatus = over.id as IssueStatus;
} else {
const targetIssue = issues.find((i) => i.id === over.id);
if (targetIssue) targetStatus = targetIssue.status;
}
if (targetStatus) {
const currentIssue = issues.find((i) => i.id === issueId);
if (currentIssue && currentIssue.status !== targetStatus) {
onMoveIssue(issueId, targetStatus);
}
}
},
[issues, onMoveIssue, visibleStatuses]
);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-1 min-h-0 gap-3 overflow-x-auto p-4">
{visibleStatuses.map((status) => (
<DroppableColumn
key={status}
status={status}
issues={issues.filter((i) => i.status === status)}
/>
))}
</div>
<DragOverlay>
{activeIssue ? (
<div className="w-64 rotate-2 opacity-90 shadow-lg">
<BoardCardContent issue={activeIssue} />
</div>
) : null}
</DragOverlay>
</DndContext>
);
}
// ---------------------------------------------------------------------------
// List View
// ---------------------------------------------------------------------------
function ListRow({ issue }: { issue: Issue }) {
const { getActorName, getActorInitials } = useActorName();
return (
<Link
href={`/issues/${issue.id}`}
className="flex h-9 items-center gap-2 px-4 text-[13px] transition-colors hover:bg-accent/50"
>
<PriorityIcon priority={issue.priority} />
<span className="w-16 shrink-0 text-xs text-muted-foreground">
{issue.id.slice(0, 8)}
</span>
<StatusIcon status={issue.status} className="h-3.5 w-3.5" />
<span className="min-w-0 flex-1 truncate">{issue.title}</span>
{issue.due_date && (
<span className="shrink-0 text-xs text-muted-foreground">
{formatDate(issue.due_date)}
</span>
)}
{issue.assignee_type && issue.assignee_id && (
<ActorAvatar
actorType={issue.assignee_type}
actorId={issue.assignee_id}
size={20}
getName={getActorName}
getInitials={getActorInitials}
/>
)}
</Link>
);
}
function ListView({ issues }: { issues: Issue[] }) {
const groupOrder = STATUS_ORDER.filter((s) => s !== "cancelled");
return (
<div className="overflow-y-auto">
{groupOrder.map((status) => {
const cfg = STATUS_CONFIG[status];
const filtered = issues.filter((i) => i.status === status);
if (filtered.length === 0) return null;
return (
<div key={status}>
<div className="flex h-8 items-center gap-2 border-b px-4">
<StatusIcon status={status} className="h-3.5 w-3.5" />
<span className="text-xs font-medium">{cfg.label}</span>
<span className="text-xs text-muted-foreground">{filtered.length}</span>
</div>
{filtered.map((issue) => (
<ListRow key={issue.id} issue={issue} />
))}
</div>
);
})}
</div>
);
}
// ---------------------------------------------------------------------------
// Create Issue Dialog
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
type ViewMode = "board" | "list";
export default function IssuesPage() {
const [view, setView] = useState<ViewMode>("board");
const [filterStatus, setFilterStatus] = useState<IssueStatus | "">("");
const [filterPriority, setFilterPriority] = useState<IssuePriority | "">("");
// Read from global store (populated by workspace hydrate + useRealtimeSync)
const allIssues = useIssueStore((s) => s.issues);
const loading = useIssueStore((s) => s.loading);
// Apply local filters
const issues = useMemo(() => {
return allIssues.filter((issue) => {
if (filterStatus && issue.status !== filterStatus) return false;
if (filterPriority && issue.priority !== filterPriority) return false;
return true;
});
}, [allIssues, filterStatus, filterPriority]);
const handleMoveIssue = useCallback(
(issueId: string, newStatus: IssueStatus) => {
// Optimistic update in store
useIssueStore.getState().updateIssue(issueId, { status: newStatus });
// Persist to API
api.updateIssue(issueId, { status: newStatus }).catch((err) => {
toast.error("Failed to move issue");
// Revert on error by refetching
api.listIssues({ limit: 200 }).then((res) => {
useIssueStore.getState().setIssues(res.issues);
});
});
},
[]
);
if (loading) {
return (
<div className="flex flex-1 min-h-0 flex-col">
<div className="flex h-11 shrink-0 items-center justify-between border-b px-4">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-8 w-24" />
</div>
<div className="flex flex-1 gap-3 overflow-x-auto p-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex min-w-52 flex-1 flex-col gap-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-24 w-full rounded-lg" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
))}
</div>
</div>
);
}
return (
<div className="flex flex-1 min-h-0 flex-col">
{/* Toolbar */}
<div className="flex h-11 shrink-0 items-center justify-between border-b px-4">
<div className="flex items-center gap-2">
<h1 className="text-sm font-semibold">All Issues</h1>
<div className="ml-2 flex items-center rounded-md border p-0.5">
<Button
variant="ghost"
size="xs"
onClick={() => setView("board")}
className={
view === "board"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
}
>
<Columns3 className="h-3 w-3" />
Board
</Button>
<Button
variant="ghost"
size="xs"
onClick={() => setView("list")}
className={
view === "list"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
}
>
<List className="h-3 w-3" />
List
</Button>
</div>
<div className="flex items-center gap-2">
<Select value={filterStatus || undefined} onValueChange={(v) => setFilterStatus((v ?? "") as IssueStatus | "")}>
<SelectTrigger size="sm" className="text-xs">
<SelectValue placeholder="All Status" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="">All Status</SelectItem>
{ALL_STATUSES.map((s) => (
<SelectItem key={s} value={s}>{STATUS_CONFIG[s].label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Select value={filterPriority || undefined} onValueChange={(v) => setFilterPriority((v ?? "") as IssuePriority | "")}>
<SelectTrigger size="sm" className="text-xs">
<SelectValue placeholder="All Priority" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="">All Priority</SelectItem>
{PRIORITY_ORDER.map((p) => (
<SelectItem key={p} value={p}>{PRIORITY_CONFIG[p].label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<Button size="sm" onClick={() => useModalStore.getState().open("create-issue")}>
<Plus className="h-3.5 w-3.5" />
New Issue
</Button>
</div>
<div className="flex-1 overflow-hidden">
{issues.length === 0 && !loading ? (
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
<p>No matching issues</p>
{(filterStatus || filterPriority) && (
<button
className="text-xs text-primary hover:underline"
onClick={() => { setFilterStatus(""); setFilterPriority(""); }}
>
Clear filters
</button>
)}
</div>
) : view === "board" ? (
<BoardView issues={issues} onMoveIssue={handleMoveIssue} />
) : (
<ListView issues={issues} />
)}
</div>
</div>
);
export default function Page() {
return <IssuesPage />;
}

View File

@@ -1,345 +0,0 @@
"use client";
import { useState } from "react";
import {
FileText,
Plus,
Search,
Link as LinkIcon,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const hours = Math.floor(diff / 3600000);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
interface KBDocument {
id: string;
title: string;
content: string;
createdBy: string;
updatedAt: string;
referencedBy: string[];
}
// ---------------------------------------------------------------------------
// Simple Markdown-ish renderer (handles headers, code blocks, tables, lists)
// ---------------------------------------------------------------------------
function renderMarkdown(text: string): React.ReactNode[] {
const lines = text.split("\n");
const elements: React.ReactNode[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i]!;
// Code block
if (line.startsWith("```")) {
const lang = line.slice(3).trim();
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i]!.startsWith("```")) {
codeLines.push(lines[i]!);
i++;
}
i++; // skip closing ```
elements.push(
<pre
key={`code-${i}`}
className="my-3 overflow-x-auto rounded-md bg-muted px-4 py-3 text-[13px] leading-relaxed"
>
<code>{codeLines.join("\n")}</code>
</pre>
);
continue;
}
// Table (simplified: detect | pipes)
if (line.includes("|") && line.trim().startsWith("|")) {
const tableRows: string[] = [];
while (i < lines.length && lines[i]!.includes("|") && lines[i]!.trim().startsWith("|")) {
tableRows.push(lines[i]!);
i++;
}
// Filter out separator rows (|---|---|)
const dataRows = tableRows.filter((r) => !r.match(/^\|[\s-|]+\|$/));
if (dataRows.length > 0) {
const parseRow = (row: string) =>
row.split("|").filter((c) => c.trim() !== "").map((c) => c.trim());
const header = parseRow(dataRows[0]!);
const body = dataRows.slice(1).map(parseRow);
elements.push(
<div key={`table-${i}`} className="my-3 overflow-x-auto">
<table className="w-full text-[13px]">
<thead>
<tr className="border-b">
{header.map((h, hi) => (
<th key={hi} className="py-1.5 pr-4 text-left font-medium">
{h}
</th>
))}
</tr>
</thead>
<tbody>
{body.map((row, ri) => (
<tr key={ri} className="border-b last:border-0">
{row.map((cell, ci) => (
<td key={ci} className="py-1.5 pr-4 text-foreground/80">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
continue;
}
// Heading
if (line.startsWith("## ")) {
elements.push(
<h2 key={`h2-${i}`} className="mt-6 mb-2 text-[15px] font-semibold">
{line.slice(3)}
</h2>,
);
i++;
continue;
}
if (line.startsWith("### ")) {
elements.push(
<h3 key={`h3-${i}`} className="mt-4 mb-1.5 text-[14px] font-medium">
{line.slice(4)}
</h3>,
);
i++;
continue;
}
// List item
if (/^- \[[ x]\] /.test(line)) {
const checked = line.includes("[x]");
const text = line.replace(/^- \[[ x]\] /, "");
elements.push(
<div key={`check-${i}`} className="flex items-center gap-2 py-0.5 text-[13px] text-foreground/80">
<input type="checkbox" checked={checked} readOnly className="h-3.5 w-3.5 rounded" />
<span>{text}</span>
</div>
);
i++;
continue;
}
if (line.startsWith("- ")) {
elements.push(
<div key={`li-${i}`} className="flex gap-2 py-0.5 text-[13px] text-foreground/80">
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-foreground/40" />
<span>{renderInline(line.slice(2))}</span>
</div>
);
i++;
continue;
}
// Numbered list
if (/^\d+\. /.test(line)) {
const num = line.match(/^(\d+)\. /)![1]!;
const text = line.replace(/^\d+\. /, "");
elements.push(
<div key={`ol-${i}`} className="flex gap-2 py-0.5 text-[13px] text-foreground/80">
<span className="w-4 shrink-0 text-right text-muted-foreground">{num}.</span>
<span>{text}</span>
</div>
);
i++;
continue;
}
// Empty line — guard is redundant since line is already asserted, but keeps TS happy
if (line.trim() === "") {
elements.push(<div key={`br-${i}`} className="h-2" />);
i++;
continue;
}
// Paragraph
elements.push(
<p key={`p-${i}`} className="text-[13px] leading-[1.7] text-foreground/85">
{renderInline(line)}
</p>
);
i++;
}
return elements;
}
function renderInline(text: string): React.ReactNode {
// Handle inline code `...`
const parts = text.split(/(`[^`]+`)/);
return parts.map((part, i) => {
if (part.startsWith("`") && part.endsWith("`")) {
return (
<code key={i} className="rounded bg-muted px-1 py-0.5 text-[12px]">
{part.slice(1, -1)}
</code>
);
}
return part;
});
}
// ---------------------------------------------------------------------------
// Components
// ---------------------------------------------------------------------------
function DocListItem({
doc,
isSelected,
onClick,
}: {
doc: KBDocument;
isSelected: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={`flex w-full items-start gap-2.5 px-4 py-2.5 text-left transition-colors ${
isSelected ? "bg-accent" : "hover:bg-accent/50"
}`}
>
<FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium">{doc.title}</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
<span>{doc.createdBy}</span>
<span>·</span>
<span>{timeAgo(doc.updatedAt)}</span>
</div>
</div>
</button>
);
}
function DocDetail({ doc }: { doc: KBDocument }) {
return (
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-3xl px-8 py-8">
{/* Title */}
<h1 className="text-xl font-semibold tracking-tight">{doc.title}</h1>
{/* Meta */}
<div className="mt-2 flex items-center gap-3 text-[12px] text-muted-foreground">
<span>By {doc.createdBy}</span>
<span>·</span>
<span>Updated {timeAgo(doc.updatedAt)}</span>
</div>
{/* Content */}
<div className="mt-6">{renderMarkdown(doc.content)}</div>
{/* Referenced by */}
{doc.referencedBy.length > 0 && (
<div className="mt-10 border-t pt-4">
<div className="flex items-center gap-1.5 text-[12px] text-muted-foreground">
<LinkIcon className="h-3 w-3" />
<span>Referenced by</span>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{doc.referencedBy.map((ref) => (
<span
key={ref}
className="rounded bg-muted px-2 py-0.5 text-[12px] font-mono"
>
{ref}
</span>
))}
</div>
</div>
)}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default function KnowledgeBasePage() {
const [documents] = useState<KBDocument[]>([]);
const [selectedId, setSelectedId] = useState<string>("");
const [search, setSearch] = useState("");
const filtered = search
? documents.filter((d) =>
d.title.toLowerCase().includes(search.toLowerCase())
)
: documents;
const selected = documents.find((d) => d.id === selectedId) ?? null;
return (
<div className="flex flex-1 min-h-0">
{/* Left: Document list */}
<div className="w-72 shrink-0 overflow-y-auto border-r">
<div className="flex h-11 items-center justify-between border-b px-4">
<h1 className="text-sm font-semibold">Knowledge Base</h1>
<Button variant="ghost" size="icon-xs">
<Plus className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
{/* Search */}
<div className="border-b px-3 py-2">
<div className="flex items-center gap-2 rounded-md border bg-background px-2.5 py-1.5">
<Search className="h-3.5 w-3.5 text-muted-foreground" />
<Input
type="text"
placeholder="Search docs..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="border-0 bg-transparent shadow-none focus-visible:ring-0 flex-1 text-[13px]"
/>
</div>
</div>
{/* Document list */}
<div className="divide-y">
{filtered.map((doc) => (
<DocListItem
key={doc.id}
doc={doc}
isSelected={doc.id === selectedId}
onClick={() => setSelectedId(doc.id)}
/>
))}
{filtered.length === 0 && (
<div className="px-4 py-8 text-center text-[13px] text-muted-foreground">
No documents found
</div>
)}
</div>
</div>
{/* Right: Document content */}
{selected ? (
<DocDetail doc={selected} />
) : (
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
Select a document
</div>
)}
</div>
);
}

View File

@@ -1,43 +1,18 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { MulticaIcon } from "@/components/multica-icon";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { AppSidebar } from "./_components/app-sidebar";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const workspace = useWorkspaceStore((s) => s.workspace);
useEffect(() => {
if (!isLoading && !user) {
router.push("/login");
}
}, [user, isLoading, router]);
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6" />
</div>
);
}
if (!user || !workspace) return null;
import { DashboardLayout } from "@multica/views/layout";
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
import { SearchCommand, SearchTrigger } from "@multica/views/search";
import { ChatFab, ChatWindow } from "@multica/views/chat";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
<DashboardLayout
loadingIndicator={<MulticaIcon className="size-6" />}
searchSlot={<SearchTrigger />}
extra={<><SearchCommand /><ChatWindow /><ChatFab /></>}
>
{children}
</DashboardLayout>
);
}

View File

@@ -0,0 +1,28 @@
import { Skeleton } from "@multica/ui/components/ui/skeleton";
export default function DashboardLoading() {
return (
<div className="flex flex-1 min-h-0 flex-col">
{/* Header skeleton */}
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-4">
<Skeleton className="h-5 w-5 rounded" />
<Skeleton className="h-4 w-32" />
</div>
{/* Toolbar skeleton */}
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-8 w-24" />
</div>
{/* Content skeleton */}
<div className="flex-1 p-4 space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-center gap-3">
<Skeleton className="h-4 w-4 rounded" />
<Skeleton className="h-4 flex-1 max-w-md" />
<Skeleton className="h-4 w-16" />
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
"use client";
import { MyIssuesPage } from "@multica/views/my-issues";
export default function Page() {
return <MyIssuesPage />;
}

View File

@@ -0,0 +1,13 @@
"use client";
import { use } from "react";
import { ProjectDetail } from "@multica/views/projects/components";
export default function ProjectDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
return <ProjectDetail projectId={id} />;
}

View File

@@ -0,0 +1,7 @@
"use client";
import { ProjectsPage } from "@multica/views/projects/components";
export default function Page() {
return <ProjectsPage />;
}

View File

@@ -0,0 +1 @@
export { RuntimesPage as default } from "@multica/views/runtimes";

View File

@@ -1,529 +1 @@
"use client";
import { useEffect, useState } from "react";
import { Settings, Users, Building2, Save, Crown, Shield, User, Plus, Trash2, LogOut } from "lucide-react";
import type { MemberWithUser, MemberRole } from "@multica/types";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { toast } from "sonner";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { api } from "@/shared/api";
const roleConfig: Record<MemberRole, { label: string; icon: typeof Crown }> = {
owner: { label: "Owner", icon: Crown },
admin: { label: "Admin", icon: Shield },
member: { label: "Member", icon: User },
};
function MemberRow({
member,
canManage,
canManageOwners,
isSelf,
busy,
onRoleChange,
onRemove,
}: {
member: MemberWithUser;
canManage: boolean;
canManageOwners: boolean;
isSelf: boolean;
busy: boolean;
onRoleChange: (role: MemberRole) => void;
onRemove: () => void;
}) {
const rc = roleConfig[member.role];
const RoleIcon = rc.icon;
const canEditRole = canManage && (!isSelf || canManageOwners) && (member.role !== "owner" || canManageOwners);
const canRemove = canManage && !isSelf && (member.role !== "owner" || canManageOwners);
return (
<div className="flex items-center gap-3 rounded-lg border px-4 py-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold">
{member.name
.split(" ")
.map((w) => w[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">{member.name}</div>
<div className="text-xs text-muted-foreground truncate">{member.email}</div>
</div>
{canEditRole ? (
<Select value={member.role} onValueChange={(value) => onRoleChange(value as MemberRole)} disabled={busy}>
<SelectTrigger size="sm"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
{canManageOwners && <SelectItem value="owner">Owner</SelectItem>}
</SelectContent>
</Select>
) : (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<RoleIcon className="h-3 w-3" />
{rc.label}
</div>
)}
{canRemove && (
<Button
variant="ghost"
size="icon-sm"
onClick={onRemove}
disabled={busy}
aria-label={`Remove ${member.name}`}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div>
);
}
export default function SettingsPage() {
const user = useAuthStore((s) => s.user);
const setUser = useAuthStore((s) => s.setUser);
const workspace = useWorkspaceStore((s) => s.workspace);
const members = useWorkspaceStore((s) => s.members);
const updateWorkspace = useWorkspaceStore((s) => s.updateWorkspace);
const refreshMembers = useWorkspaceStore((s) => s.refreshMembers);
const leaveWorkspace = useWorkspaceStore((s) => s.leaveWorkspace);
const deleteWorkspace = useWorkspaceStore((s) => s.deleteWorkspace);
const [name, setName] = useState(workspace?.name ?? "");
const [description, setDescription] = useState(
workspace?.description ?? "",
);
const [context, setContext] = useState(workspace?.context ?? "");
const [profileName, setProfileName] = useState(user?.name ?? "");
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url ?? "");
const [saving, setSaving] = useState(false);
const [profileSaving, setProfileSaving] = useState(false);
const [inviteEmail, setInviteEmail] = useState("");
const [inviteRole, setInviteRole] = useState<MemberRole>("member");
const [inviteLoading, setInviteLoading] = useState(false);
const [memberActionId, setMemberActionId] = useState<string | null>(null);
const [confirmAction, setConfirmAction] = useState<{
title: string;
description: string;
variant?: "destructive";
onConfirm: () => Promise<void>;
} | null>(null);
const currentMember = members.find((member) => member.user_id === user?.id) ?? null;
const canManageWorkspace = currentMember?.role === "owner" || currentMember?.role === "admin";
const isOwner = currentMember?.role === "owner";
useEffect(() => {
setName(workspace?.name ?? "");
setDescription(workspace?.description ?? "");
setContext(workspace?.context ?? "");
}, [workspace]);
useEffect(() => {
setProfileName(user?.name ?? "");
setAvatarUrl(user?.avatar_url ?? "");
}, [user]);
const handleSave = async () => {
if (!workspace) return;
setSaving(true);
try {
const updated = await api.updateWorkspace(workspace.id, {
name,
description,
context,
});
updateWorkspace(updated);
toast.success("Workspace settings saved");
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to save workspace settings");
} finally {
setSaving(false);
}
};
const handleProfileSave = async () => {
setProfileSaving(true);
try {
const updated = await api.updateMe({
name: profileName,
avatar_url: avatarUrl || undefined,
});
setUser(updated);
toast.success("Profile updated");
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to update profile");
} finally {
setProfileSaving(false);
}
};
const handleAddMember = async () => {
if (!workspace) return;
setInviteLoading(true);
try {
await api.createMember(workspace.id, {
email: inviteEmail,
role: inviteRole,
});
setInviteEmail("");
setInviteRole("member");
await refreshMembers();
toast.success("Member added");
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to add member");
} finally {
setInviteLoading(false);
}
};
const handleRoleChange = async (memberId: string, role: MemberRole) => {
if (!workspace) return;
setMemberActionId(memberId);
try {
await api.updateMember(workspace.id, memberId, { role });
await refreshMembers();
toast.success("Role updated");
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to update member");
} finally {
setMemberActionId(null);
}
};
const handleRemoveMember = (member: MemberWithUser) => {
if (!workspace) return;
setConfirmAction({
title: `Remove ${member.name}`,
description: `Remove ${member.name} from ${workspace.name}? They will lose access to this workspace.`,
variant: "destructive",
onConfirm: async () => {
setMemberActionId(member.id);
try {
await api.deleteMember(workspace.id, member.id);
await refreshMembers();
toast.success("Member removed");
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to remove member");
} finally {
setMemberActionId(null);
}
},
});
};
const handleLeaveWorkspace = () => {
if (!workspace) return;
setConfirmAction({
title: "Leave workspace",
description: `Leave ${workspace.name}? You will lose access until re-invited.`,
variant: "destructive",
onConfirm: async () => {
setMemberActionId("leave");
try {
await leaveWorkspace(workspace.id);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to leave workspace");
} finally {
setMemberActionId(null);
}
},
});
};
const handleDeleteWorkspace = () => {
if (!workspace) return;
setConfirmAction({
title: "Delete workspace",
description: `Delete ${workspace.name}? This cannot be undone. All issues, agents, and data will be permanently removed.`,
variant: "destructive",
onConfirm: async () => {
setMemberActionId("delete-workspace");
try {
await deleteWorkspace(workspace.id);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to delete workspace");
} finally {
setMemberActionId(null);
}
},
});
};
if (!workspace) return null;
return (
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="mx-auto max-w-2xl p-6 space-y-8">
{/* Page header */}
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Settings</h1>
</div>
<section className="space-y-4">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Profile</h2>
</div>
<div className="space-y-3 rounded-lg border p-4">
<div>
<Label className="text-xs text-muted-foreground">
Name
</Label>
<Input
type="search"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Avatar URL
</Label>
<Input
type="url"
value={avatarUrl}
onChange={(e) => setAvatarUrl(e.target.value)}
placeholder="https://example.com/avatar.png"
className="mt-1"
/>
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<Button
size="sm"
onClick={handleProfileSave}
disabled={profileSaving || !profileName.trim()}
>
<Save className="h-3 w-3" />
{profileSaving ? "Updating..." : "Update Profile"}
</Button>
</div>
</div>
</section>
{/* Workspace info */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Workspace</h2>
</div>
<div className="space-y-3 rounded-lg border p-4">
<div>
<Label className="text-xs text-muted-foreground">
Name
</Label>
<Input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={!canManageWorkspace}
className="mt-1"
/>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Description
</Label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
disabled={!canManageWorkspace}
className="mt-1 resize-none"
placeholder="What does this workspace focus on?"
/>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Context
</Label>
<Textarea
value={context}
onChange={(e) => setContext(e.target.value)}
rows={4}
disabled={!canManageWorkspace}
className="mt-1 resize-none"
placeholder="Background information and context for AI agents working in this workspace"
/>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Slug
</Label>
<div className="mt-1 rounded-md border bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
{workspace.slug}
</div>
</div>
<div className="flex items-center justify-end gap-2 pt-1">
<Button
size="sm"
onClick={handleSave}
disabled={saving || !name.trim() || !canManageWorkspace}
>
<Save className="h-3 w-3" />
{saving ? "Saving..." : "Save"}
</Button>
</div>
{!canManageWorkspace && (
<p className="text-xs text-muted-foreground">
Only admins and owners can update workspace settings.
</p>
)}
</div>
</section>
{/* Members */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">
Members ({members.length})
</h2>
</div>
</div>
{canManageWorkspace && (
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center gap-2">
<Plus className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Add member</h3>
</div>
<div className="grid gap-3 sm:grid-cols-[1fr_120px_auto]">
<Input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="user@company.com"
/>
<Select value={inviteRole} onValueChange={(value) => setInviteRole(value as MemberRole)}>
<SelectTrigger size="sm"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
{isOwner && <SelectItem value="owner">Owner</SelectItem>}
</SelectContent>
</Select>
<Button
onClick={handleAddMember}
disabled={inviteLoading || !inviteEmail.trim()}
>
{inviteLoading ? "Adding..." : "Add"}
</Button>
</div>
</div>
)}
<div className="space-y-2">
{members.map((m) => (
<MemberRow
key={m.id}
member={m}
canManage={canManageWorkspace}
canManageOwners={isOwner}
isSelf={m.user_id === user?.id}
busy={memberActionId === m.id}
onRoleChange={(role) => handleRoleChange(m.id, role)}
onRemove={() => handleRemoveMember(m)}
/>
))}
{members.length === 0 && (
<p className="text-sm text-muted-foreground">No members found.</p>
)}
</div>
</section>
<section className="space-y-4">
<div className="flex items-center gap-2">
<LogOut className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Danger Zone</h2>
</div>
<div className="rounded-lg border p-4 space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium">Leave workspace</p>
<p className="text-xs text-muted-foreground">
Remove yourself from this workspace.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleLeaveWorkspace}
disabled={memberActionId === "leave"}
>
{memberActionId === "leave" ? "Leaving..." : "Leave workspace"}
</Button>
</div>
{isOwner && (
<div className="flex flex-col gap-2 border-t pt-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium text-destructive">Delete workspace</p>
<p className="text-xs text-muted-foreground">
Permanently delete this workspace and its data.
</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={handleDeleteWorkspace}
disabled={memberActionId === "delete-workspace"}
>
{memberActionId === "delete-workspace" ? "Deleting..." : "Delete workspace"}
</Button>
</div>
)}
</div>
</section>
<AlertDialog open={!!confirmAction} onOpenChange={(v) => { if (!v) setConfirmAction(null); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{confirmAction?.title}</AlertDialogTitle>
<AlertDialogDescription>{confirmAction?.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={confirmAction?.variant === "destructive" ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : ""}
onClick={async () => {
await confirmAction?.onConfirm();
setConfirmAction(null);
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
);
}
export { SettingsPage as default } from "@multica/views/settings";

Some files were not shown because too many files have changed in this diff Show More