Compare commits

...

766 Commits

Author SHA1 Message Date
Jiang Bohan
d7a4c3a1aa feat(agent): log full command line at debug level when spawning agents
Add a debug-level log line in every agent backend (claude, codex,
opencode, openclaw, gemini, hermes) that prints the executable path
and full argument list when spawning the agent process. Helps diagnose
custom args, model overrides, and other CLI flag issues.
2026-04-15 16:20:12 +08:00
Bohan Jiang
129a8b927f fix(views): auto-split whitespace in custom args entries (#1065)
Users naturally type `--model claude-sonnet-4-20250514` on one line,
but the backend needs them as separate tokens. Now `entriesToArgs`
splits each entry by whitespace before saving, so the API receives
`["--model", "claude-sonnet-4-20250514"]` instead of a single string.

Also updated placeholder and description to show the natural input
format.
2026-04-15 15:17:06 +08:00
Bohan Jiang
ce447c7f06 feat(agent): add custom CLI arguments support (#986)
* feat(agent): add custom CLI arguments support

Allow users to configure custom CLI arguments per agent that get
appended to the agent subprocess command at launch time. This enables
use cases like specifying different models (--model o3), max turns,
or other provider-specific flags without needing separate runtimes.

Changes:
- Add custom_args JSONB column to agent table (migration 041)
- Update API handler to accept/return custom_args in create/update
- Pass custom_args through claim endpoint to daemon
- Append custom_args to CLI commands for all agent backends
- Add ExecOptions.CustomArgs field in agent package
- Add Custom Args tab in agent detail UI
- Add --custom-args flag to CLI agent create/update commands

Closes MUL-802

* fix(agent): filter protocol-critical flags from custom_args

Add per-backend filtering of custom_args to prevent users from
accidentally overriding flags that the daemon hardcodes for its
communication protocol (e.g. --output-format, --input-format,
--permission-mode for Claude).

This follows the same pattern as custom_env's isBlockedEnvKey: we
only block the small, stable set of flags that would break the
daemon↔agent protocol — not every possible dangerous flag. Workspace
members are trusted for everything else.

Each backend defines its own blocked set:
- Claude: -p, --output-format, --input-format, --permission-mode
- Gemini: -p, --yolo, -o
- Codex: --listen
- OpenCode: --format
- OpenClaw: --local, --json, --session-id, --message
- Hermes: none (ACP is positional)

Includes unit tests for the filtering logic.

* fix(agent): address code review nits for custom_args

- Replace module-level `nextArgId` counter with `crypto.randomUUID()`
  in custom-args-tab.tsx to avoid SSR ID conflicts
- Add unit tests for custom args passthrough and blocked-arg filtering
  in both Claude and Gemini arg builders
2026-04-15 14:58:53 +08:00
LinYushen
5dad1f0915 fix(selfhost): clear hardcoded NEXT_PUBLIC_API_URL/WS_URL defaults (#1063)
The .env.example had hardcoded http://localhost:8080 defaults for
NEXT_PUBLIC_API_URL and NEXT_PUBLIC_WS_URL. When users copied .env.example
to .env and customized the backend port, the old defaults would still get
baked into the frontend at docker build time via NEXT_PUBLIC_WS_URL build
arg, causing API/WebSocket connection failures.

With empty defaults:
- Docker selfhost: frontend uses relative paths, Next.js rewrites proxy
  to backend internally — works regardless of external port config
- Local dev (make dev): Makefile sets these to localhost:$PORT automatically
- Browser fallback: deriveWsUrl() auto-derives WebSocket URL from page
  origin when NEXT_PUBLIC_WS_URL is empty

Closes #1055

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:56:30 +08:00
LinYushen
c0db3e0e76 Revert "feat(selfhost): add single-domain Caddy setup (#899)" (#1062)
This reverts commit 100146c49e.
2026-04-15 14:44:47 +08:00
LinYushen
6bbe059055 feat(desktop): sync package version with CLI via git tag at build time (#1050)
* fix(desktop): ship entitlements.mac.plist so electron-builder can codesign

electron-builder.yml already references build/entitlements.mac.plist
via entitlementsInherit, but the file was missing from the tree, so
`pnpm package` failed at the codesign step with:

  build/entitlements.mac.plist: cannot read entitlement data

Ship the file. It grants the hardened-runtime capabilities the app
actually needs: JIT + unsigned executable memory for V8, disabled
library validation so the Electron process can spawn the bundled
`multica` Go binary as a child process, and network client/server for
the daemon's API and /health endpoints.

Also tweak the root .gitignore: the top-level `build` rule was
shadowing apps/desktop/build/, hiding this config file from git.
Add a scoped exception so apps/desktop/build/ (which holds
electron-builder source resources, not output) is tracked.

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

* feat(desktop): derive package version from git tag at build time

The Desktop app version was hardcoded to "0.1.0" in package.json and
never bumped, while the bundled CLI reports whatever `git describe`
gives at build time. Result: packaging on main produced
desktop-0.1.0.dmg containing multica v0.1.35-14-gf1415e96 — completely
disconnected. Users see two unrelated version numbers for the same
release.

Sync them by using the same source GoReleaser uses for the CLI: the
nearest git tag. A new scripts/package.mjs wrapper runs bundle-cli.mjs,
derives the version via `git describe --tags --always --dirty` (strips
the `v` prefix, falls back to `0.0.0-<hash>` when no tags are
reachable), and invokes electron-builder with
`-c.extraMetadata.version=<derived>` — which overrides package.json at
build time without mutating the tracked file.

On a clean tag commit → "0.1.36"; between tags → "0.1.35-14-gf1415e96"
(valid semver prerelease); dirty tree → same with "-dirty" suffix.

The `package` script in package.json now points to the wrapper.
Passthrough args (--mac, --arm64, etc.) after `pnpm package --` are
forwarded to electron-builder unchanged. Dev and build scripts are
untouched — they continue to use bundle-cli.mjs directly.

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

* feat(desktop): enable macOS notarization and clean artifact names

Two electron-builder.yml tweaks that unblock a proper release:

- `mac.notarize: false` → `true`. Notarization runs in-build via
  notarytool, reading APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID
  from env. electron-builder then staples the ticket before zipping, so
  `latest-mac.yml`'s SHA512s match the published artifacts (critical
  for electron-updater — post-hoc re-stapling would invalidate them).
  Non-mac/CI contributors are unaffected: `pnpm package` already
  requires the Developer ID signing cert, and notarization is a strict
  superset of signing.

- `mac.artifactName` and `dmg.artifactName` now hardcode
  `multica-desktop-${version}-${arch}.${ext}` instead of using
  `${name}`, which expands to `@multica/desktop` for scoped package
  names and literally produced files at `dist/@multica/desktop-*.dmg`.
  The nested `@multica/` path is useless and makes the GitHub Release
  asset URL ugly. New layout is flat: `dist/multica-desktop-<ver>-arm64.dmg`.

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

* fix(desktop): keep local package builds working after notarize: true

Three polish items from review of this PR.

- Local dev regression: `mac.notarize: true` in electron-builder.yml
  made `pnpm package` hard-fail on macs without APPLE_* env vars, even
  for non-publishing local smoke tests. Detect the missing env in
  scripts/package.mjs and pass `-c.mac.notarize=false` for that run
  only. Real release builds (which source apps/desktop/macOS/.env via
  the release-desktop skill) are unaffected. Also logs a clear warning
  so the developer knows notarization was skipped.
- spawnSync previously used `shell: true`, which reassembled argv into
  a shell command string. Zero real-world injection risk given our
  controlled inputs, but dropping it closes the vector at no cost —
  pnpm already puts node_modules/.bin on PATH for script runs so the
  binary is found without a shell wrapper.
- On spawn failure (e.g. electron-builder not found), result.error was
  silently swallowed and the exit was just `1`. Log the underlying
  reason before exiting.

Also refactor so normalizeGitVersion is exportable and guard the main
entry behind an import.meta.url check, enabling unit coverage. New
package.test.mjs covers the six branches: null/empty input, clean tag,
between-tags prerelease, dirty suffix, v-prefixed prerelease tags
(vX.Y.Z-alpha and vX.Y.Z-rc.2), and the 0.0.0-<hash> fallback for
hash-only describe output. vitest.config.ts picks up scripts/**/*.test.mjs.

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

* feat(desktop): commit .env.production for release builds

Bake production backend + app URLs into release packages so `pnpm
package` produces a build that points at multica.ai out of the box.
electron-vite (Vite) reads .env.production automatically in production
mode — no script changes needed.

Values:

  VITE_API_URL   = https://api.multica.ai
  VITE_WS_URL    = wss://api.multica.ai/ws
  VITE_APP_URL   = https://multica.ai

Also parameterize the two hardcoded `https://www.multica.ai` strings
in platform/navigation.tsx's `getShareableUrl` on VITE_APP_URL. The
previous hardcoded host pointed to `www.multica.ai`, which disagrees
with the canonical `multica.ai` we're standardizing on. Shareable
links from the desktop ("Copy link to issue") now match.

The env file is public config, not a secret, so add a scoped exception
to the root .gitignore's `.env*` rule.

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-15 14:12:53 +08:00
Naiyuan Qing
cf70860a0b Merge pull request #1052 from multica-ai/NevilleQingNY/fix-bubble-menu-pos
fix(editor): fix bubble menu positioning on first selection
2026-04-15 13:57:15 +08:00
Naiyuan Qing
9f350e312d Merge pull request #1053 from multica-ai/agent/agent/bbde5dd5
fix(cli): add pagination metadata to issue list and update agent prompt
2026-04-15 13:53:03 +08:00
Naiyuan Qing
08c3513eef fix(cli): add pagination metadata to issue list JSON output and update agent prompt
Issue list JSON now includes total, limit, offset, has_more fields so agents
can detect truncated results and paginate. Also documents --limit/--offset in
the agent prompt and emphasizes mention format in Output section.

Closes MUL-837

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:51:08 +08:00
Naiyuan Qing
817e69a9eb fix(editor): fix bubble menu positioning on first selection
Tiptap's React wrapper initialises the menu element with
position:absolute, but computePosition needs position:fixed so
getOffsetParent returns the viewport instead of a positioned ancestor.
On the first show, coordinates were computed relative to the wrong
containing block, causing the menu to fly off-screen (negative coords).

Fix: set position:fixed in the onShow callback, which fires right
before updatePosition(), ensuring computePosition sees the correct
offset parent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:49:33 +08:00
Jiayuan Zhang
f94b0100cd refactor(autopilot): remove broken concurrency policies and fix multiple bugs (#1048)
Remove the concurrency_policy system (skip/queue/replace) — skip had an
orphan bug that permanently blocked triggers, queue didn't actually queue,
and replace didn't cancel running tasks. Every trigger now simply executes.

Bug fixes:
- Listener now handles in_review status (was silently ignored)
- Issue deletion fails linked autopilot runs before DELETE (prevents orphans)
- ComputeNextRun rejects invalid timezones instead of silent UTC fallback
- dispatchCreateIssue post-commit failures now properly fail the run

Reliability:
- Scheduler recovers lost triggers on startup (crash recovery)
- New index on autopilot_run(issue_id) for deletion lookups
- Migration 043 cleans up historical orphaned/skipped/pending runs
2026-04-15 13:48:21 +08:00
marcel
287a9eb546 fix(repocache): pass explicit env to remote-facing git subprocesses (#1029)
fix(repocache): pass explicit env to remote-facing git subprocesses
2026-04-15 13:15:36 +08:00
Bohan Jiang
45dad23074 fix(views): sort timeline entries after WebSocket append (#1047)
WebSocket event handlers for comment:created and activity:created
appended new entries to the end of the timeline array without sorting.
When events arrived out of order (e.g. agent replying rapidly), comments
displayed out of chronological order.

Sort the timeline by created_at after each append to maintain correct
chronological ordering.

Closes #1032
2026-04-15 13:07:45 +08:00
Bohan Jiang
762e64d469 fix(agent): restrict custom_env visibility to owner/admin (#1046)
* fix(agent): restrict custom_env visibility to agent owner and workspace admin

Agent environment variables (custom_env) were visible to all workspace
members, exposing sensitive tokens. Now only the agent owner and
workspace owner/admin can view them — regular members receive the field
omitted (null) from API responses, and the frontend hides the
Environment tab accordingly.

Closes #1018

* fix(agent): show masked env keys to non-authorized users instead of hiding tab

Instead of completely hiding the Environment tab for non-owner/non-admin
users, show the variable keys with masked values (****) in a read-only
view. This lets members see which variables are configured without
exposing the actual values.

- Backend: mask values with "****" instead of nullifying custom_env
- Added custom_env_redacted boolean to API response
- Frontend: EnvTab supports readOnly mode with lock icon and muted styling
2026-04-15 13:06:49 +08:00
devv-eve
f1415e9622 fix(sidebar): narrow user popover width (#1045)
* feat(sidebar): replace user menu ellipsis with full-row popover

Remove the three-dot menu from the sidebar footer user profile.
The entire row is now clickable and opens an upward popover showing
the user's full name, email, and a logout button.

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

* fix(sidebar): narrow user popover width

Reduce popover from w-64 to w-48 and tighten internal spacing
to better fit the sidebar proportions.

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>
Co-authored-by: yushen <ldnvnbl@gmail.com>
2026-04-15 12:24:42 +08:00
LinYushen
8030f1adbc feat(desktop): restart local daemon when bundled CLI version differs (#1041)
* feat(desktop): restart local daemon when bundled CLI version differs

Desktop bundles a multica CLI binary at build time via bundle-cli.mjs.
If a local daemon is already running from a previous session with an
older CLI, the newly bundled version never takes effect until the user
manually restarts. Fix that on the login/auto-start path.

- Expose the daemon's CLI version on GET /health as cli_version (sourced
  from cfg.CLIVersion, which is already set from the ldflag at daemon
  startup in cmd_daemon.go).
- In the desktop main process, query the resolved CLI binary's version
  once via `multica version --output json` and cache it for the process
  lifetime.
- On daemon:auto-start, if the daemon is already running, compare the
  two versions. Restart only when BOTH sides are known and the strings
  differ — a restart kills in-flight agent tasks, so any uncertainty
  (bundled CLI unknown, older daemon without cli_version field, read
  failure) fails safe and leaves the daemon alone.

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

* feat(daemon): defer version-mismatch restart until active tasks drain

Previous iteration restarted the daemon immediately on a confirmed CLI
version mismatch, which would kill any agent tasks mid-execution. Gate
the restart on an active-task counter so in-flight work always finishes.

- Daemon: add `activeTasks atomic.Int64` on the Daemon struct,
  increment/decrement it around handleTask, and expose it as
  `active_task_count` on GET /health.
- Desktop: when a version mismatch is confirmed but active_task_count >
  0, set a pendingVersionRestart flag instead of restarting. The 5s
  pollOnce loop retries ensureRunningDaemonVersionMatches on each tick
  and fires the restart the moment the count drops to 0.
- Eventual consistency: if the user keeps the daemon permanently busy,
  the version stays out of date — that's a strictly better failure mode
  than silently killing hour-long agent runs.

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

* test(daemon): cover version-check decision + /health counter exposure

Addresses the test-coverage gap from the second review.

- Go: extract the /health handler into a named method `(d *Daemon)
  healthHandler(startedAt time.Time)` so it can be exercised via
  httptest without spinning up a listener. Add health_test.go covering
  cli_version + active_task_count field exposure and the increment /
  decrement protocol used by pollLoop.
- Desktop: extract the pure version-check decision logic into
  version-decision.ts (no electron, no I/O, no module state). The
  ensureRunningDaemonVersionMatches wrapper now delegates the "what
  should we do" decision to decideVersionAction and owns only the side
  effects (logging, flag mutation, restartDaemon call).
- Desktop: bolt vitest onto apps/desktop (vitest.config.ts + catalog
  devDep + test script) so main-process unit tests have a home. Add
  version-decision.test.ts covering all four action branches and the
  busy→idle drain transition.

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

* fix(daemon): bust CLI version cache on retry-install, lock wire-level JSON keys

Two polish items from review.

- daemon:retry-install now also clears cachedCliBinaryVersion. Previously
  a retry that landed a newly-downloaded CLI at a different version
  would false-negative on the next version check because the cached
  version string was sticky for the process lifetime.
- TestHealthHandlerReportsCLIVersionAndActiveTaskCount now decodes into
  a raw map[string]any and asserts the exact snake_case keys
  (cli_version, active_task_count, status). The desktop TS client keys
  on these literal strings, so a silent struct-tag rename must fail the
  test. Typed struct round-trip kept as a separate value check.

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-15 12:19:01 +08:00
devv-eve
eacf33299a feat(sidebar): replace user menu ellipsis with full-row popover (#1044)
Remove the three-dot menu from the sidebar footer user profile.
The entire row is now clickable and opens an upward popover showing
the user's full name, email, and a logout button.

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:15:10 +08:00
devv-eve
cf012b2706 feat(agents): show runtime owner and Mine/All filter in Create Agent dialog (#1042)
* feat(agents): show runtime owner and add Mine/All filter in Create Agent dialog

Display the runtime owner (with avatar) in the runtime selector dropdown,
matching the pattern used in the Runtime list page. Add a Mine/All toggle
to filter runtimes by ownership, defaulting to "Mine" so the current user's
runtimes appear first.

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

* feat(agents): show runtime owner and Mine/All filter in agent Settings tab

Apply the same owner display and Mine/All filter pattern to the Settings
tab's runtime selector, matching the Create Agent dialog. Uses ProviderLogo
and ActorAvatar for consistent runtime item rendering across both selectors.

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

* fix(agents): address PR review — use unfiltered runtimes for lookup, simplify IIFE

- Look up selectedRuntime from full `runtimes` array instead of
  `filteredRuntimes` to avoid null flash when switching filters
- Replace IIFE with inline optional chaining for owner name display
- Fix indentation on the trigger subtitle div

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-14 20:57:53 -07:00
devv-eve
2cbebfc568 refactor(daemon): remove watch/unwatch workspace logic, default to all workspaces (#1003)
The daemon now automatically watches all workspaces the user belongs to,
fetched directly from the API. This removes the manual watch/unwatch
workflow, the config-based watched/unwatched lists, the /watch HTTP
endpoints, the CLI watch/unwatch commands, and the desktop app's watched
workspace UI and reconciliation logic.

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:24:15 +08:00
KimSeongJun
100146c49e feat(selfhost): add single-domain Caddy setup (#899)
* selfhost: add single-domain caddy setup

* fix(selfhost): address Caddy review feedback
2026-04-14 20:20:26 -07:00
Naiyuan Qing
de982f3a4e Merge pull request #1037 from multica-ai/NevilleQingNY/editor-arch-review
refactor(editor): remove hardcoded CDN domain, unify file card rendering
2026-04-15 10:47:19 +08:00
Naiyuan Qing
53cb01cc91 refactor(editor): remove hardcoded CDN domain, unify file card rendering
- Add GET /api/config endpoint exposing cdn_domain from CLOUDFRONT_DOMAIN
- Create packages/core/config/ zustand store, fetched at app startup
- Extract file card preprocessing to packages/ui/markdown/file-cards.ts
  with isCdnUrl(url, cdnDomain) using exact hostname match
- Add file card support to packages/ui/markdown/Markdown.tsx (was missing)
- Remove hardcoded .copilothub.ai hostname check from file-card.tsx
- Fix LocalStorage.CdnDomain() to return hostname not full URL
- Always run preprocessFileCards regardless of cdnDomain availability
  (!file syntax works without CDN domain, only legacy matching needs it)
- Use useConfigStore hook in common/markdown.tsx for reactive updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:43:36 +08:00
Naiyuan Qing
afa711b442 Merge pull request #1031 from multica-ai/NevilleQingNY/editor-arch-review
fix(editor): hover card bug, view crash, perf, and link handler cleanup
2026-04-15 09:30:41 +08:00
Naiyuan Qing
8d6e5f2bcc fix(editor): hover card bug, view crash, perf, and link handler cleanup
- Fix issue mention cards incorrectly triggering Link Hover Card
- Guard editor.view access in BubbleMenu against unmounted/destroyed
  view Proxy (fixes desktop Inbox fast-switching crash)
- Use useEditorState for precise formatting state subscriptions in
  BubbleMenu instead of relying on parent re-renders
- Add markdownTokenizer to FileCard for unambiguous !file[name](url)
  roundtrip syntax (legacy CDN hostname matching kept for compat)
- Extract shared openLink/isMentionHref into utils/link-handler.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:27:01 +08:00
Naiyuan Qing
c460206846 Merge pull request #1030 from multica-ai/feat/inter-font-cjk-fallback
fix(fonts): Inter + CJK fallback to fix full-width punctuation rendering
2026-04-15 08:49:20 +08:00
Naiyuan Qing
70e4f44860 style(fonts): add text-autospace for CJK+Latin auto-spacing and sync design doc
- packages/ui/styles/base.css: add `text-autospace: ideograph-alpha
  ideograph-numeric` to html. Native CSS feature (Chrome 119+,
  Electron recent) that auto-inserts 1/4em space between CJK ideographs
  and Latin letters/numerals. Progressive enhancement — older browsers
  ignore the rule silently.
- docs/design.md: update font family table to reflect Inter + CJK system
  fallback. Reword font-bold ban rationale to be font-agnostic
  (information density / layout rhythm), not Geist-specific.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:45:23 +08:00
Naiyuan Qing
4b10c9354a fix(fonts): swap Geist Sans → Inter with explicit CJK fallback
Full-width Chinese punctuation (e.g. ,) was rendering at Latin-font
metrics, making it look half-width in the editor. Root cause: Geist is
Latin-only, and neither web (next/font) nor desktop (@fontsource) declared
any CJK fallback, so CJK chars inherited Geist's em-box width through
Chromium's per-character fallback.

- Web (apps/web/app/layout.tsx): Geist → Inter via next/font/google,
  with explicit fallback array: system fonts → PingFang SC (macOS) →
  Microsoft YaHei (Windows) → Noto Sans CJK SC (Linux) → sans-serif.
- Desktop: removed @fontsource/geist-sans, added @fontsource-variable/inter
  (single variable-weight file replaces 4 static weights). Updated
  --font-sans in globals.css to match web's fallback chain.
- Geist Mono kept for code blocks; mono chain has no CJK fallback by
  design (CJK is non-aligned in mono grids, listing CJK fonts would
  falsely signal alignment guarantees). Added Consolas to web mono for
  Windows symmetry with desktop.
- Cross-reference sync comments in both layout.tsx and globals.css:
  CJK tail must stay in sync; Inter primary differs by design (next/font
  injects `__Inter_xxx` with adjustFontFallback metric override;
  fontsource uses raw "Inter Variable").

Currently covers English + Simplified Chinese. When ja/ko i18n lands,
extend fallback tails with Hiragino Kaku Gothic ProN / Yu Gothic /
Apple SD Gothic Neo / Malgun Gothic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:45:14 +08:00
Jiayuan Zhang
d88fe2608e feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents

Introduce the Autopilot feature — recurring automations that assign work
to AI agents on a schedule or manual trigger. Supports two execution
modes: create_issue (creates an issue for the agent to work on) and
run_only (directly enqueues an agent task without issue pollution).

Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService
with concurrency policies (skip/queue/replace), HTTP CRUD + trigger
endpoints, background cron scheduler (30s tick), event listeners for
issue→run and task→run status sync.

Frontend: types, API client methods, TanStack Query hooks with optimistic
mutations, realtime cache invalidation, list page with create dialog,
detail page with trigger management and run history, sidebar nav + routes
for both web and desktop apps.

* feat(autopilot): improve UX — trigger config, edit dialog, template gallery

- Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone
- Fix Select components showing UUIDs instead of names (Base UI render function pattern)
- Add Edit button on detail page opening a unified edit dialog
- Remove project/concurrency/issue-title-template from create/edit (simplify for users)
- Add trigger configuration inline during autopilot creation
- Add template gallery on empty state (6 step-by-step workflow templates)
- Rename "Description" to "Prompt" throughout UI
- Inject autopilot run timestamp into issue description for agent date awareness
- Treat issue status "in_review" as run completion (fixes skip on next trigger)
- Make migration idempotent with IF NOT EXISTS clauses
2026-04-15 04:54:37 +08:00
Bohan Jiang
c79cfaf330 fix(auth): honor ?next= redirect through Google OAuth flow (#1024)
The login page now encodes the ?next= param into the Google OAuth state
so the auth callback can redirect to the right destination (e.g.
/invite/{id}) after login, instead of always going to /issues.
2026-04-15 00:52:12 +08:00
Bohan Jiang
60c5848794 feat(invitation): dedicated /invite/{id} page for accepting invitations (#1023)
The email CTA now deep-links to /invite/{id} instead of the generic app
URL. If the user isn't logged in, they're redirected to login with a
?next= param that brings them back to the invite page.

Changes:
- Backend: GET /api/invitations/{id} endpoint (enriched with workspace/inviter names)
- Backend: Email template now links to /invite/{invitationId}
- Frontend: Shared InvitePage component (packages/views/invite/)
- Frontend: Web route at (auth)/invite/[id], Desktop route at invite/:id
- Frontend: /invite/ excluded from navigation history persistence
2026-04-15 00:37:53 +08:00
Bohan Jiang
642c6ae5ee docs: add Gemini CLI to all documentation and landing page (#1022)
Gemini CLI support was added to the backend in v0.1.33 but was missing
from all user-facing documentation and the website. Added Gemini CLI
(and Hermes where missing) to the agents table, quickstart guides,
CLI reference, installation docs, self-hosting guide, and landing page
hero section with logo.
2026-04-15 00:28:02 +08:00
Bohan Jiang
1163f684fb feat(invitation): send email notification when inviting a user (#1021)
Uses the existing Resend email service to notify invitees.
Email includes inviter name, workspace name, and a link to the app.
Sent fire-and-forget in a goroutine to avoid blocking the API response.
2026-04-15 00:17:21 +08:00
Bohan Jiang
ff1d348274 feat(security): invitation acceptance flow for workspace members (#1019)
* feat(security): replace instant member-add with invitation acceptance flow

Users invited to a workspace must now explicitly accept the invitation
before becoming a member. This fixes the security vulnerability where
knowing someone's email was enough to auto-register their runtime to
your workspace.

Changes:
- Add workspace_invitation table with pending/accepted/declined/expired states
- Replace CreateMember with CreateInvitation (same endpoint, new behavior)
- Add accept/decline/revoke/list invitation API endpoints
- Add invitation WS events for real-time notification
- Frontend: invitation accept/decline UI in workspace switcher
- Frontend: pending invitations section in members settings tab

* fix(invitation): address PR review nits

- Fix invitation:revoked listener to send event to invitee user (was no-op)
- Remove duplicate queryClient2 in app-sidebar.tsx, reuse existing queryClient
- Add expires_at > now() filter to ListPendingInvitationsByWorkspace query
2026-04-15 00:01:18 +08:00
Bohan Jiang
b4b69f89f6 fix(server): allow members to create and manage their own skills (#1017)
Remove admin/owner-only restriction from skill creation and import routes.
Add canManageSkill helper that lets skill creators manage their own skills,
matching the existing canManageAgent pattern for agents.
2026-04-14 23:48:57 +08:00
Bohan Jiang
a3c6f07668 fix(server): allow members to create agents (#1013)
Remove the owner/admin role restriction on the POST /api/agents endpoint
so that workspace members can also create agents.
2026-04-14 22:51:59 +08:00
Asish Kumar
b2649fb47f fix(realtime): add WebSocket ping/pong heartbeat to detect dead connections (#917)
Without a heartbeat, dead or silently-dropped WebSocket connections are
not detected until the next write fails. This causes goroutine and memory
leaks for each stale client, and breaks real-time updates for users whose
connections are dropped by a load balancer or proxy idle timeout (e.g.
Nginx default 60s, AWS ALB default 60s) without a TCP RST.

This commit applies the standard gorilla/websocket keepalive pattern:

- writePump sends a ping frame every pingPeriod (54 s) using a ticker.
  The ticker replaces the simple range-over-channel loop with a select,
  which also adds a proper write deadline on every write operation.

- readPump installs a pong handler that resets the read deadline on each
  pong, keeping healthy connections alive indefinitely.  A connection
  that misses a pong is detected within pongWait (60 s) and closed,
  which causes readPump to exit and send the client to hub.unregister
  for clean removal.

Timing constants:
  writeWait  = 10 s  (per-write deadline, prevents hung writers)
  pongWait   = 60 s  (max silence before declaring a connection dead)
  pingPeriod = 54 s  (ping interval, 90 % of pongWait)

Also adds user_id and workspace_id to the write-error log line so that
connection problems can be attributed to a specific client in production.

All existing hub tests continue to pass unchanged.

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-14 21:56:06 +08:00
Bohan Jiang
c2a5ed73e8 fix(web): add /uploads/* rewrite for self-hosted deployments (#1010)
On self-hosted deployments where the frontend is the public entrypoint,
uploaded files return 404 because /uploads/* requests aren't proxied to
the backend. Add a rewrite rule following the existing pattern for /api/*,
/ws, and /auth/*.

Closes #1004
2026-04-14 21:34:47 +08:00
Jiayuan Zhang
f0c0a64ddd feat(cli): support --version and -v flags on root command (#1007)
Use Cobra's built-in version support so `multica --version` and
`multica -v` print the same output as `multica version`.

Closes MUL-743
2026-04-14 21:14:00 +08:00
Naiyuan Qing
2ecddc8fc8 Merge pull request #1002 from multica-ai/chore/remove-desktop-remote-mode
chore(desktop): remove dev:desktop:remote proxy mode
2026-04-14 19:54:43 +08:00
Naiyuan Qing
2a2e6f4746 chore(desktop): remove dev:desktop:remote proxy mode
Drops the VITE_REMOTE_API Vite-proxy path introduced in be8b099c.
The remote-backend proxy is no longer needed; direct dev via
VITE_API_URL covers every workflow we still support.

- remove dev:desktop:remote (root) and dev:remote (desktop) scripts
- revert electron.vite.config.ts to a flat config — no loadEnv, no
  per-route proxies
- simplify App.tsx: single apiBaseUrl/wsUrl branch, and
  DAEMON_TARGET_API_URL derives directly from VITE_API_URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:53:55 +08:00
Bohan Jiang
6538496ee4 fix(daemon): sync workspaces from API before failing on empty runtime list (#1001)
When the CLI config has no watched workspaces (e.g. fresh desktop app
install), loadWatchedWorkspaces returns successfully but registers zero
runtimes. The runtime check immediately after fails with "no runtimes
registered" before workspaceSyncLoop gets a chance to discover
workspaces from the API.

Run one sync cycle inline when the watched list is empty so the daemon
can bootstrap itself without a pre-configured workspace list.
2026-04-14 19:52:52 +08:00
Naiyuan Qing
69ef002bbb Merge pull request #1000 from multica-ai/fix/desktop-titlebar-drag-region
fix(desktop): stop macOS traffic lights from hijacking titlebar & modals
2026-04-14 19:48:45 +08:00
Naiyuan Qing
7dad45d444 feat(desktop): immersive mode hides traffic lights for full-screen modals
Full-screen modals (create-workspace) covered the app titlebar, so the
Back button landed on top of the macOS traffic lights — where native
hit-test always wins and the button couldn't be clicked. The modal
also swallowed the window's drag region.

Introduce a desktop IPC channel window:setImmersive that calls
BrowserWindow.setWindowButtonVisibility, exposed through the existing
desktopAPI preload bridge. A small useImmersiveMode() hook in
@multica/views/platform toggles it for the component's lifetime and
is a no-op on web / non-macOS.

CreateWorkspaceModal now:
- calls useImmersiveMode() so traffic lights disappear while it's open
- adds a transparent top h-10 drag strip to restore window dragging
- moves the Back button from top-6 left-6 to top-12 left-12 with an
  explicit no-drag region so clicks always reach it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:41:49 +08:00
Naiyuan Qing
7ade4b432d fix(desktop): pad main top-bar when sidebar collapses so tabs don't sit under traffic lights
Extract the main-area top bar into a MainTopBar component so it can
read sidebar state via useSidebar(). When the sidebar is collapsed,
apply pl-20 (80px) to the drag header so the TabBar starts clear of
the macOS traffic-light hit-test region (~x=16..68) that always
wins over HTML clicks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:41:38 +08:00
LinYushen
cbb2cf0c6c chore(desktop): rebuild CLI on every bundle-cli run (#999)
bundle-cli.mjs now invokes `go build` with the same ldflags as
`make build` (version/commit/date) before copying the binary into
resources/bin/. Running this on every `pnpm dev:desktop`, `dev:remote`
and `package` guarantees the bundled CLI matches the current Go source,
so you can't accidentally ship a stale binary after editing server/
code. Go's build cache makes no-op builds ~a few hundred ms.

Graceful fallback preserved: if `go` is not on PATH (frontend-only
contributor), we warn, skip the build, and let cli-bootstrap download
the latest release at runtime. Compile errors remain fatal so broken
Go code blocks dev rather than silently falling back.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:33:39 +08:00
Naiyuan Qing
d94b704a71 Merge pull request #993 from multica-ai/feat/chat-reading-width
fix(chat): reading-width container + refresh placeholder on agent switch
2026-04-14 19:17:06 +08:00
Naiyuan Qing
76ba9cfb0b Merge pull request #995 from multica-ai/feat/chat-skeleton
feat(chat): skeleton while switching to an un-cached session
2026-04-14 19:16:48 +08:00
devv-eve
40aa23a528 feat(desktop): daemon management panel with sidebar status bar (#952)
* feat(desktop): add daemon management panel with sidebar status bar

Integrate multica daemon lifecycle management into the desktop app so
users can start/stop/restart the daemon and view live logs without
leaving the UI. Session tokens are automatically synced to the CLI
config file, making daemon authentication transparent.

- daemon-manager.ts: Electron main process module for daemon lifecycle
  (health polling, start/stop via CLI, token sync, log tail)
- Preload bridge: new daemonAPI with IPC for all daemon operations
- Sidebar bottomSlot: persistent daemon status indicator in sidebar
  footer (desktop-only, injected via AppSidebar slot)
- Daemon panel Sheet: right-side drawer with status details, controls,
  and real-time log viewer with auto-scroll and level coloring
- Token sync: on login and app startup, JWT is written to
  ~/.multica/config.json so daemon can authenticate seamlessly

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

* feat(desktop): add P1+P2 daemon features — runtimes card, auto-start, settings

P1: Runtimes page Local Daemon card
- Add topSlot prop to shared RuntimesPage for platform injection
- DaemonRuntimeCard shows status, agents, uptime with Start/Stop/
  Restart/Logs buttons (desktop-only, injected via slot)

P2: Auto-start and auto-stop
- Daemon auto-starts on app launch when user is authenticated
  (controlled by autoStart preference, default: true)
- Daemon auto-stops on app quit (controlled by autoStop preference,
  default: false — daemon keeps running in background by default)
- Preferences persisted to ~/.multica/desktop_prefs.json

P2: Daemon settings tab
- New "Daemon" tab in Settings > My Account section (desktop-only)
- Toggle auto-start and auto-stop behavior
- CLI installation status check with link to install guide
- SettingsPage gains extraAccountTabs prop for platform injection

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

* fix(desktop): address PR review feedback on daemon management

Must-fix:
- before-quit handler now calls event.preventDefault(), awaits
  stopDaemon(), then re-calls app.quit() so the daemon actually
  stops before the app exits
- Add concurrency guard (operationInProgress lock) in daemon-manager
  to reject overlapping start/stop/restart IPC calls
- Extract shared types (DaemonState, DaemonStatus, DaemonPrefs),
  constants (STATE_COLORS, STATE_LABELS), and formatUptime to
  apps/desktop/src/shared/daemon-types.ts — all renderer components
  now import from this single source

Should-fix:
- Log viewer uses monotonic counter (LogEntry.id) instead of array
  index as React key, preventing full re-renders on overflow
- All start/stop/restart handlers now show toast.error() with the
  error message when the operation fails
- startLogTail retries up to 5 times with 2s delay when the log
  file doesn't exist yet (handles first-run case)

Minor:
- Cache findCliBinary() result after first successful lookup

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

* fix(logger): suppress ANSI color codes when stderr is not a TTY

Detect whether stderr is connected to a terminal and set tint's NoColor
option accordingly. Previously daemon.log files contained raw escape
sequences like \033[2m and \033[92m which made them unreadable in the
Desktop log viewer and any non-TTY sink (docker logs, systemd, etc).

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

* feat(daemon): runtime watch/unwatch HTTP endpoints and denylist

Add GET/POST/DELETE /watch handlers on the daemon's health port so
clients (notably Desktop) can add or remove watched workspaces at
runtime without restarting the daemon or editing config.json. Each
handler updates in-memory state under d.mu and persists back to
~/.multica/profiles/<name>/config.json for survival across restarts.

- CLIConfig gains UnwatchedWorkspaces as an explicit opt-out denylist.
  syncWorkspacesFromAPI skips entries in the denylist so a manual
  unwatch isn't silently revived 30s later by the periodic sync.
- loadWatchedWorkspaces tolerates an empty config and returns nil
  instead of erroring out, because Desktop starts daemons with a
  fresh profile and relies on the sync loop / watch endpoint to
  populate the list.

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

* feat(desktop): bundled CLI, per-backend profile, and watch UI

Make the Desktop app self-sufficient: it bundles its own multica
binary, manages its own daemon profile keyed by the backend URL, and
authenticates that daemon with a long-lived PAT it mints on first
login. The daemon panel gains a checkbox list of watched workspaces
and surfaces the active profile + server URL.

CLI bootstrap
- scripts/bundle-cli.mjs copies server/bin/multica into
  apps/desktop/resources/bin/ before electron-vite dev and
  electron-builder package. asarUnpack: resources/** already covers
  this path, so the binary ships with the .app in prod.
- main/cli-bootstrap.ts adds an ensureManagedCli() fallback that
  downloads the latest release from GitHub when no bundled binary
  exists (first launch on a machine without developer tooling).
- daemon-manager.resolveCliBinary prefers bundled > managed > download
  > PATH, so local iteration uses the freshly built binary.

Daemon profile
- resolveActiveProfile now derives a desktop-<host> profile name from
  the target API URL and creates its config.json on demand. Never
  reads or writes the user's hand-configured CLI profiles, avoiding
  the "Desktop polluted my default profile" class of bug.
- syncToken detects a JWT input and exchanges it for a PAT via
  POST /api/tokens; caches the resulting mul_* token in the profile
  config so subsequent launches skip the round-trip.
- startDaemon / stopDaemon / log tail all operate on the resolved
  profile; renderer sets the target URL via a new
  daemon:set-target-api-url IPC.

Workspace watching
- daemon-manager exposes daemon:list-watched / daemon:watch-workspace /
  daemon:unwatch-workspace IPCs backed by the daemon's new /watch
  endpoints.
- App.tsx reconciles the user's workspace list against the daemon's
  watched set whenever TanStack Query updates it — new workspaces are
  registered instantly instead of waiting for the daemon's 30s sync,
  and removed workspaces are unwatched.
- daemon-panel gains a "Watched Workspaces" section with per-workspace
  checkboxes that call watch/unwatch directly. Opt-outs persist in the
  profile's unwatched_workspaces denylist.

Lifecycle states + UI
- DaemonStatus gains `profile`, `serverUrl`, and an `installing_cli`
  state. Panel shows Profile / Server info rows and a "Setting up…"
  blurb during first-run CLI download; failure surfaces a Retry button.
- Status bar renders a spinner during installation and hides the Start
  button until setup finishes.

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

* fix(desktop): register /onboarding route

The create-workspace modal navigates to /onboarding on success, but
the Desktop router only had flat routes (issues, projects, runtimes,
etc.) — resulting in an "Unexpected Application Error! 404 Not Found"
page after creating a new workspace.

Mirror the web app's wiring: render OnboardingWizard with onComplete
pushing to /issues, via the shared navigation adapter.

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

* refactor(desktop): remove sidebar daemon status bar

Drop the bottom-left daemon indicator in favor of the DaemonRuntimeCard
at the top of the Runtimes page, which already shows the same info
plus full Start/Stop/Restart controls and the Logs entry point. A
single canonical place avoids fragmenting daemon status across the UI.

Also remove the now-unused `bottomSlot` prop from AppSidebar — Desktop
was the only consumer, Web never needed it, so keeping it would be
dead scaffolding.

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

* fix(desktop): daemon panel layout and close button

- Logs section now fills the remaining vertical space down to the
  sheet bottom instead of being capped at h-64, which left a huge
  empty area below it. Top section (status, actions, watched list)
  keeps natural height as shrink-0; the watched list gets its own
  max-h-48 scroll so a long list can't push Logs off screen.
- Replace the Sheet's built-in close button with an explicit
  <button> wired directly to onOpenChange(false). The Base UI
  Dialog.Close wrapped in Button via the render prop wasn't firing
  on click in this panel; going straight through the controlled
  state guarantees it responds.

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

* fix(desktop): make daemon panel clickable inside Electron drag region

The sheet opens at the top of the window, which visually overlaps the
TabBar's -webkit-app-region: drag zone. Even though the sheet portals
to document.body, Chromium computes drag regions over the final
composited pixels, so the sheet inherited "drag" and swallowed the
mouseup of every click (mousedown fired but click never resolved) —
including the X close button.

Mark the entire SheetContent popup with -webkit-app-region: no-drag
to subtract it from the drag region. This also fixes future buttons /
checkboxes inside the sheet that would have hit the same issue.

While here, move the close button into the SheetHeader as a flex
sibling of SheetTitle instead of an absolutely positioned overlay —
simpler layout and avoids any stacking-context weirdness.

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

* feat(desktop): clickable daemon runtime card row

The whole Local Daemon row now opens the sheet panel — icon, title,
and status line are all part of one click target. This replaces the
standalone "Logs" button, which was redundant now that clicking
anywhere on the row does the same thing.

The right-side action cluster (Start / Stop / Restart) wraps its
onClick in stopPropagation so pressing those buttons doesn't bubble
up and open the panel.

Keyboard access: Enter / Space on the focused row opens the panel,
with a focus-visible background for feedback.

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

* feat(runtimes): mark Desktop-launched daemons as managed

When the Multica Desktop app spawns the CLI it ships with, the
resulting daemon shares its binary with the Electron bundle — Desktop
is responsible for updating that binary on every release. Letting the
daemon self-update would just get clobbered on the next Desktop launch
and could brick the embedded binary mid-update.

Propagate a "launched_by" signal end-to-end so the UI can hide the
CLI self-update affordance (and the daemon refuses updates as a second
line of defense):

- Desktop's startDaemon spawns execFile with env MULTICA_LAUNCHED_BY=desktop.
- daemon.Config gains LaunchedBy; cmd_daemon reads the env var on boot.
- registerRuntimesForWorkspace includes launched_by in the request body.
- Server DaemonRegister folds launched_by into runtime.metadata (JSONB
  — no migration needed).
- handleUpdate returns a "failed" status with an explanatory message
  when LaunchedBy == "desktop", so even a bypass API call can't trigger
  the self-update path.
- RuntimeDetail extracts metadata.launched_by and passes it to
  UpdateSection, which swaps the Latest / → available / Update button
  cluster for a muted "Managed by Desktop" label.

CLI-only users (brew install, direct tarball) keep the exact same
behavior — the env var is empty, the UI shows the update button,
the daemon still self-updates on request.

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

* fix(desktop): harden daemon manager from PR review

- syncToken now takes userId and mints a fresh PAT on user switch,
  restarting a running daemon so it picks up the new credentials.
  A .desktop-user-id sidecar in each profile records the owner so a
  previous user's cached PAT can't be reused on the next login.
- App.tsx wires onLogout on CoreProvider to daemonAPI.clearToken()
  and daemonAPI.stop() so the cached PAT and live daemon don't
  outlive the session.
- startLogTail replaced with a cross-platform watchFile
  implementation (initial 32 KB window + poll for new bytes,
  handles truncation). spawn("tail") was broken on Windows.
- writeProfileConfig now serializes through a promise chain to
  prevent concurrent writes from corrupting config.json.
- startDaemon keeps the "starting" state until pollOnce confirms
  /health, avoiding a running → stopped flash when the Go daemon
  isn't yet listening after the supervisor returns.

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

* fix(desktop): verify downloaded CLI against checksums.txt

Download goreleaser's checksums.txt alongside the release archive,
parse the sha256 lookup, stream the archive through createHash, and
refuse to install on mismatch or missing entry. Closes the supply-
chain gap where auto-install would execute an unverified binary on
first launch.

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

* chore(desktop): lint and style cleanups from PR review

- eslint.config.mjs: add scripts/**/*.{mjs,js} override with
  globals.node so bundle-cli.mjs lints clean (was erroring on
  undefined process/console).
- daemon-panel.tsx: log level classes now use semantic tokens
  (text-info, text-warning, text-destructive) instead of hardcoded
  Tailwind colors; escape the apostrophe in the retry copy.
- daemon-settings-tab.tsx: import DaemonPrefs from shared/daemon-
  types instead of redefining it.
- runtimes-page.tsx: fix indentation inside the new topSlot wrapper.

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>
Co-authored-by: yushen <ldnvnbl@gmail.com>
2026-04-14 19:12:39 +08:00
Naiyuan Qing
d3f7570177 feat(chat): skeleton while switching to an un-cached session
Switching to a session whose messages aren't cached showed the empty
state (starter prompts) for the ~300ms the fetch took — jarring, because
you're clicking into an existing conversation, not starting a new one.

Now there's a three-branch render: skeleton while loading, empty state
for real new-chat (activeSessionId === null), messages when ready.
Cached switches still return data synchronously — no flash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:49:26 +08:00
Naiyuan Qing
34e452776b fix(chat): reading-width container + refresh placeholder on agent switch
- Wrap ChatMessageList and ChatInput in mx-auto max-w-4xl px-5 so wide
  chat windows don't sprawl — matches the issue-detail / project-detail
  width convention
- draftKey now includes the selected agent id in the new-chat state.
  Tiptap's Placeholder only applies at mount, so key-driven remount is
  the simplest way to refresh it when the user switches agents before
  sending the first message. Side benefit: per-agent new-chat drafts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:42:55 +08:00
Bohan Jiang
2551aa53ef fix(docs): use light theme for Star History chart in dark mode (#992)
* docs: add Trendshift GitHub Trending badge to READMEs

Add dynamic GitHub Trending badge from Trendshift.io (repo ID 24695)
to both English and Chinese READMEs, placed below existing CI/stars
badges.

* docs: replace Trendshift badge with Star History chart

Remove the Trendshift trending badge and add a Star History chart
section at the end of both English and Chinese READMEs. The chart
supports dark/light mode and links to the interactive star-history page.

* fix(docs): use light theme for Star History chart in both color schemes

Remove &theme=dark from the dark mode source so the chart always
renders with a light background regardless of GitHub's color scheme.
2026-04-14 18:34:35 +08:00
Naiyuan Qing
d779cbd183 Merge pull request #990 from multica-ai/NevilleQingNY/fix-cmdk-stale-status
fix(views): resolve stale status in cmd+k recent issues list
2026-04-14 18:26:28 +08:00
Naiyuan Qing
10b6afc1ec Merge pull request #988 from multica-ai/feat/chat-redesign
feat(chat): redesign state, header, and unread tracking
2026-04-14 18:24:20 +08:00
Bohan Jiang
4f58f0c8eb docs: add Star History chart to READMEs (#989)
* docs: add Trendshift GitHub Trending badge to READMEs

Add dynamic GitHub Trending badge from Trendshift.io (repo ID 24695)
to both English and Chinese READMEs, placed below existing CI/stars
badges.

* docs: replace Trendshift badge with Star History chart

Remove the Trendshift trending badge and add a Star History chart
section at the end of both English and Chinese READMEs. The chart
supports dark/light mode and links to the interactive star-history page.
2026-04-14 18:24:15 +08:00
Naiyuan Qing
0399e387f8 fix(views): resolve stale status in cmd+k recent issues list
Recent issues store was duplicating server data (title, status, identifier)
in Zustand, violating the single-source-of-truth architecture. Now the store
only tracks visit records (id + visitedAt), and the search command joins
fresh data from the TanStack Query issue list cache at render time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:23:27 +08:00
Naiyuan Qing
a744cd4f45 feat(chat): redesign state, header, and unread tracking
State management
- Pending task / live timeline are now Query-cache single source;
  Zustand mirror removed (fixes duplicate assistant render caused by
  the invalidate→refetch race window)
- WS subscriptions moved from ChatWindow to global useRealtimeSync so
  pending state survives minimize and refresh
- New GET /chat/sessions/:id/pending-task to recover live state on mount
- Drafts persisted per-session (was per-workspace)

Unread tracking
- Migration 040: chat_session.unread_since (event-driven; old chats
  stay clean — no mass backfill)
- POST /chat/sessions/:id/read clears unread; broadcasts
  chat:session_read so other devices sync
- New GET /chat/pending-tasks aggregate for the FAB
- ChatFab: brand-color impulse animation while running, brand-dot
  badge of unread session count
- ChatWindow auto-marks read when user is viewing the session

Header redesign
- Two independent dropdowns: agent (avatar + name + My/Others
  grouping) at the input bottom-left; session (title + agent avatar)
  in the header
- ⊕ new-chat button replaces the old + and history buttons
- Session dropdown lists all sessions across agents with avatars
- Empty state: 3 clickable starter prompts that send immediately
- Mention link renderer falls through to default span on null —
  fixes @member/@agent/@all silently disappearing app-wide
- User messages render through Markdown
- Enter submits in chat input only (with IME guard + codeBlock skip);
  bubble menu hidden in chat

Misc
- Partial index on agent_task_queue for fast pending-task lookup
- 2 new storage keys added to clearWorkspaceStorage
- useMarkChatSessionRead has onError rollback
- chat.* namespace logs across store, mutations, components, realtime

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:21:11 +08:00
Jiayuan Zhang
bfa9bec8c4 docs: add Multica vs Paperclip comparison to README (#980) 2026-04-14 18:06:44 +08:00
Bohan Jiang
bf71802451 fix(server): trigger agent on reply in thread where agent already participated (#981)
When a member replies in a member-started thread without @mentioning the
assigned agent, the on_comment trigger was suppressed — even if the agent
had already replied in that thread. This meant the common flow of
"member posts → agent replies → member follows up" would not re-trigger
the agent on the follow-up.

Add HasAgentRepliedInThread SQL query and check it in isReplyToMemberThread
so that agent participation in a thread is treated as an ongoing conversation.
2026-04-14 18:00:29 +08:00
Jiayuan Zhang
09e6190400 fix(cli): use localhost for CLI callback when app URL is a public hostname (#977)
When `multica login` runs against production (multica.ai), the CLI was
using the app URL hostname as the callback host, producing a callback
URL like `http://multica.ai:PORT/callback`. This URL fails frontend
validation (which only allows localhost and private IPs) and can't
actually reach the CLI's local HTTP server.

Now only private IPs (RFC 1918) are used as the callback host, which
matches the intended self-hosted LAN scenario. Public hostnames
correctly fall back to localhost.

Fixes #974
2026-04-14 17:54:10 +08:00
Naiyuan Qing
0798b5f8bb Merge pull request #982 from multica-ai/agent/agent/85c11509
fix(views): prevent focus jump on modal close
2026-04-14 17:53:51 +08:00
Naiyuan Qing
e568896357 fix(views): prevent focus jumping to random button when closing store-opened modals
Add finalFocus={false} to DialogContent in create-issue and create-workspace
modals so Base UI does not attempt focus restoration on close. These modals
are always opened programmatically via useModalStore (no trigger element),
so there is no meaningful element to return focus to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:52:16 +08:00
Bohan Jiang
8748557c7b fix: clarify local vs workspace skills to reduce user confusion (#975)
* fix: clarify that local skills work automatically, workspace skills are for team sharing

Users were confused, thinking they needed to re-upload locally installed skills
(e.g. .claude/skills/) to Multica before agents could use them. Updated UI text
across the skills page, agent skills tab, onboarding, and docs to clearly
distinguish between local skills (auto-discovered) and workspace skills (for
team-wide sharing).

Closes #972

* feat(views): replace inline text with info banner for local skills hint

The "local runtime skills are always available" message was buried in
the description text and easy to miss. Move it into a visible info
callout banner with an icon so users notice it immediately.
2026-04-14 17:49:34 +08:00
Bohan Jiang
7f0c23a6ba Merge pull request #960 from blackhu0804/test/cli-client-context-headers
test(cli): cover API client context headers
2026-04-14 17:14:12 +08:00
Bohan Jiang
e6767d2ba3 Merge pull request #968 from multica-ai/agent/j/0ae3c9f0
docs: add manual testing checklist to PR template
2026-04-14 17:06:30 +08:00
Bohan Jiang
1ceb75e218 Update PULL_REQUEST_TEMPLATE.md 2026-04-14 17:06:13 +08:00
Jiang Bohan
9138c05993 docs: revise PR checklist to match Paperclip-style format
Replace the original checklist + manual testing section with a
unified checklist modeled after the Paperclip open-source project:
thinking path, model disclosure, local tests, test coverage,
UI screenshots, documentation, risk assessment, and reviewer comments.
2026-04-14 17:04:18 +08:00
Naiyuan Qing
091ed7370a Merge pull request #953 from multica-ai/fix/editor-bubble-menu-v2
fix(editor): fix BubbleMenu dropdown clicks by replacing DropdownMenu with Popover
2026-04-14 16:47:18 +08:00
Naiyuan Qing
35557c0b11 fix(test): add missing selection mock in ContentEditor test
The merge from main introduced `editor?.state.selection.empty` in
ContentEditor. The test mock was missing `state.selection`, causing
a TypeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:44:35 +08:00
Naiyuan Qing
03ad47200b merge: resolve conflict with main (accept link-preview.tsx deletion)
# Conflicts:
#	packages/views/editor/link-preview.tsx
2026-04-14 16:39:24 +08:00
Bohan Jiang
93b754de53 Merge pull request #969 from multica-ai/agent/j/696a5ce1
docs: add v0.1.33 changelog (2026-04-14)
2026-04-14 16:37:08 +08:00
Jiang Bohan
609d2e06ae docs: remove desktop auto-update from v0.1.33 changelog 2026-04-14 16:35:59 +08:00
Jiang Bohan
7c436c0dcb docs: add v0.1.33 changelog entry (2026-04-14) 2026-04-14 16:33:41 +08:00
Naiyuan Qing
55ae78b902 fix(editor): replace DropdownMenu with Popover in BubbleMenu to fix focus
Base UI's DropdownMenu uses FloatingFocusManager which steals focus from
the editor (initialFocus + closeOnFocusOut), causing the BubbleMenu to
hide before dropdown item clicks can register. Popover supports
initialFocus={false} and finalFocus={false}, keeping editor focus intact
throughout the interaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:32:02 +08:00
Jiang Bohan
cc00fda513 docs: add manual testing/acceptance checklist to PR template
Adds a new "Manual Testing / Acceptance" section to the PR template
with checklist items for verifying changes in a real environment:
happy path, edge cases, visual regressions, cross-platform testing,
API consumer compatibility, and log inspection.
2026-04-14 16:25:55 +08:00
Bohan Jiang
04e571b02f Merge pull request #964 from multica-ai/feat/agent-env-tab
feat(views): extract environment variables into separate agent tab
2026-04-14 16:18:12 +08:00
Jiang Bohan
c62bd0ca12 feat(views): extract environment variables into separate agent tab
Move the Environment Variables section from the Settings tab into its
own "Environment" tab (KeyRound icon) between Tasks and Settings. Each
tab now has independent save state.
2026-04-14 16:06:06 +08:00
Bohan Jiang
51c7dbbeee Merge pull request #962 from multica-ai/fix/editor-link-preview-mount-crash
fix(editor): avoid accessing editor.view during initial render in link preview
2026-04-14 15:48:38 +08:00
Jiang Bohan
46d745cb60 fix(editor): avoid accessing editor.view during initial render in link preview
EditorLinkPreview's useRef initializer accessed editor.view?.dom which
throws when the editor view is not yet mounted (Tiptap uses a Proxy
that rejects property access before mount). Defer the contextElement
assignment to the selectionUpdate callback where the view is guaranteed
to exist.
2026-04-14 15:47:52 +08:00
Bohan Jiang
0a998d1cef Merge pull request #846 from multica-ai/agent/j/feb218fd
feat(agent): support custom environment variables for router/proxy mode
2026-04-14 15:34:21 +08:00
Bohan Jiang
a366984014 Merge pull request #961 from multica-ai/fix/comment-trigger-new-tag
fix(daemon): emphasize NEW comment in trigger prompt to prevent session confusion
2026-04-14 15:27:43 +08:00
Jiang Bohan
9ba9ea66f8 fix(daemon): emphasize NEW comment in trigger prompt to prevent session confusion
When a comment-triggered task resumes an existing session, the agent
may mistake the new comment for a previous one and skip it. Add [NEW
COMMENT] tag to the prompt and reinforce in AGENTS.md workflow that
the agent must respond to THIS specific comment, not prior ones.
2026-04-14 15:26:49 +08:00
Bohan Jiang
2be6fdae90 Merge pull request #956 from yyy9942/fix/cancel-task-race-condition
fix(server): handle cancel request for already-completed tasks gracefully
2026-04-14 15:20:44 +08:00
Bohan Jiang
653c0adeee Merge pull request #959 from multica-ai/agent/j/e52c9eda
fix(views): issue mentions missing status/title after page refresh
2026-04-14 15:18:06 +08:00
yyy9942
4458753102 fix(server): handle cancel request for already-completed tasks gracefully
When a task finishes between the UI rendering the Stop button and the
user clicking it, CancelAgentTask returns no rows. Previously this
surfaced as a 400 error. Now CancelTask checks for pgx.ErrNoRows and
returns the current task state instead of an error.

Closes #954
2026-04-14 16:15:22 +09:00
black-fe
3c0ed0f732 test(cli): cover API client context headers 2026-04-14 15:13:54 +08:00
Naiyuan Qing
999d0728c5 fix(editor): remove preventDefault from dropdown triggers in BubbleMenu
The onMouseDown preventDefault on HeadingDropdown and ListDropdown
triggers was interfering with Base UI's menu event flow, causing:
- Dropdown appearing at top-left corner (positioning mismatch)
- Menu item clicks not applying formatting

The BubbleMenu plugin's own preventHide mechanism (capture-phase
mousedown listener) already handles preventing the menu from hiding
during dropdown interaction. Our extra preventDefault was redundant
and conflicting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:13:32 +08:00
Jiang Bohan
b6a69c113e fix(views): fetch individual issue for mentions not in list cache
Issue mentions in comments showed only the identifier (no status icon
or title) after page refresh when the referenced issue wasn't in the
issueListOptions cache (e.g. done issues beyond the first 50).

Fall back to issueDetailOptions to fetch the individual issue when it's
not found in the list. The detail query is only enabled when the issue
is missing from the list, so it adds no overhead for the common case.
2026-04-14 15:03:42 +08:00
Bohan Jiang
7995f7368f Merge pull request #957 from multica-ai/agent/j/9ecd3271
fix(issues): include done issues in parent/sub-issue picker
2026-04-14 14:56:47 +08:00
Jiang Bohan
ed1a1dc6b1 fix(issues): include done/cancelled issues in parent/sub-issue picker search
The IssuePickerDialog was not passing include_closed: true to searchIssues,
so done and cancelled issues were invisible in the picker.
2026-04-14 14:55:42 +08:00
Naiyuan Qing
97755ae45d feat(editor): add link hover card with URL preview and actions
Show a floating card on link hover with truncated URL, Copy and Open
buttons. Uses @floating-ui/dom computePosition portaled to body
(escapes overflow:hidden). 300ms show delay, 150ms hide delay with
card hover support.

- New link-hover-card.tsx with useLinkHover hook + LinkHoverCard
- Integrated in ContentEditor (disabled when BubbleMenu active)
- Integrated in ReadonlyContent (always active)
- Styled with popover design tokens (matches bubble-menu)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:53:32 +08:00
Bohan Jiang
7a896d3852 Merge pull request #897 from multica-ai/agent/j/177ad75f
fix(openclaw): handle pretty-printed multi-line JSON output
2026-04-14 14:49:04 +08:00
Bohan Jiang
da63165cdc Merge pull request #955 from multica-ai/agent/j/3c269006
fix(issues): update UI immediately when parent/sub-issue changes
2026-04-14 14:48:38 +08:00
Jiang Bohan
013584ef80 fix(issues): invalidate new parent's children cache on parent_issue_id change
Both the useUpdateIssue mutation and the WS onIssueUpdated handler only
invalidated the OLD parent's children query. When parent_issue_id changes,
the new parent's sub-issues list was stale until page refresh.
2026-04-14 14:46:55 +08:00
Jiang Bohan
bb4944bae2 fix(openclaw): handle pretty-printed multi-line JSON output
OpenClaw outputs its --json result as pretty-printed multi-line JSON to
stderr. The line-by-line scanner never found a valid JSON object on any
single line, causing the raw JSON to be returned as the chat response.

After exhausting line-by-line parsing, try parsing the accumulated
output as a whole before falling back to raw text.

Closes MUL-725
2026-04-14 14:39:32 +08:00
Bohan Jiang
42e392c727 Merge pull request #950 from multica-ai/agent/j/6b9aa53b
feat(cli): add --parent flag to issue update command
2026-04-14 14:37:12 +08:00
Bohan Jiang
158a100779 Merge pull request #949 from multica-ai/agent/j/73a6b30b
feat(issues): add parent/sub-issue linking via More menu
2026-04-14 14:36:59 +08:00
Naiyuan Qing
e178682acd fix(editor): use native BubbleMenu and simplify link click
BubbleMenu:
- Replace custom useFloating + createPortal with Tiptap's native
  <BubbleMenu> component (battle-tested focus management)
- Add scrollTarget (auto-detect nearest scroll container) so the
  plugin repositions on scroll
- Add scroll-aware display:none for nested container clipping
  (plugin's hide middleware can't detect it — virtual element has
  no contextElement)
- Add .trim() to textBetween check to filter whitespace-only selections
- Enable hide middleware for viewport-level hiding

Link click:
- Both editable and readonly modes now open links directly
- Remove EditorLinkPreview component and link-preview.tsx entirely
- ReadonlyContent links use same direct-open pattern

Cleanup:
- Delete link-preview.tsx (not needed)
- Remove @floating-ui/react-dom dependency
- Remove .link-preview-card CSS
- Add @tiptap/extension-bubble-menu dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:34:52 +08:00
Bohan Jiang
8779db976c Merge pull request #948 from multica-ai/agent/j/400a618f
feat(agent): add live log support for Gemini CLI
2026-04-14 14:31:52 +08:00
Jiang Bohan
eba68c15fd feat(cli): add --parent flag to issue update command
Allows setting or clearing an issue's parent via the CLI:
  multica issue update <id> --parent <parent-id>
  multica issue update <id> --parent ""  # clear parent
2026-04-14 14:24:19 +08:00
Jiang Bohan
345cb984a9 feat(issues): add "Set parent issue" and "Add sub-issue" to More menu
Add two new options to the issue detail More dropdown that let users
link existing issues as parent or sub-issue via a search dialog.
2026-04-14 14:20:48 +08:00
Jiang Bohan
f3355049bc feat(agent): add live log support for Gemini CLI via stream-json
Switch Gemini backend from `-o text` (batch output) to `-o stream-json`
(NDJSON streaming) so tool calls, text, and errors are forwarded to the
UI in real time instead of collected at the end.

Parses all Gemini stream-json event types: init, message, tool_use,
tool_result, error, and result — including per-model token usage from
the result stats.
2026-04-14 14:17:13 +08:00
Naiyuan Qing
dca86acc69 Merge pull request #938 from 1WorldCapture/fix/lyo-7-description-click-focus
fix(views): focus description editor when clicking empty area
2026-04-14 14:06:32 +08:00
Bohan Jiang
c71525e198 Merge pull request #910 from multica-ai/agent/j/openclaw-p0-p1
feat(agent): OpenClaw backend P0+P1 improvements
2026-04-14 14:02:38 +08:00
devv-eve
977dc6479d fix(daemon): prevent task stall when agent process hangs on stdout (#947)
When an agent CLI process hangs (e.g. a tool call blocks on unreachable
I/O), the daemon's scanner blocks indefinitely on stdout, preventing the
Result from ever being sent. This causes tasks to stay in "running"
state permanently with no further events.

Three-layer fix:

1. Agent backends (claude, opencode, openclaw, gemini): add a watchdog
   goroutine that closes the stdout/stderr pipe when the context is
   cancelled, forcing the scanner to unblock. Also set cmd.WaitDelay
   so Go force-closes pipes after 10s if the process doesn't exit.

2. daemon executeAndDrain: add an independent drain timeout (backend
   timeout + 30s buffer) with context-aware select on both the message
   channel and the result channel, so the daemon never blocks forever.

3. daemon ping path: add context-aware select so pings don't deadlock
   if the agent backend stalls.

Closes #925

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 23:00:27 -07:00
Jiayuan Zhang
a97bd3da0b fix(auth): support non-localhost CLI callback for self-hosted VMs (#944)
The CLI auth callback was hardcoded to localhost, breaking self-hosted
setups where the browser runs on a different machine than the CLI.

- CLI: derive callback host from configured app URL; bind to 0.0.0.0
  when the app URL is not localhost so remote browsers can reach it
- Frontend: expand validateCliCallback to accept RFC 1918 private IPs
  (10.x, 172.16-31.x, 192.168.x) in addition to localhost

Closes #923
2026-04-14 13:50:02 +08:00
Jiayuan Zhang
9dfe119f47 fix(daemon): use runtime's owner_id for agent migration on upgrade (#941)
* fix(daemon): prevent duplicate runtime registration on profile switch

The daemon_id included a profile name suffix (e.g. "hostname-staging"),
so switching profiles created a new daemon_id that bypassed the UPSERT
dedup constraint, leaving orphaned runtime records in the database.

Three changes:
- Remove profile suffix from daemon_id — use stable hostname only.
  The unique constraint (workspace_id, daemon_id, provider) already
  prevents collisions within the same workspace.
- Auto-migrate agents from old offline runtimes to the newly registered
  runtime during DaemonRegister (same workspace/provider/owner).
- Add TTL-based GC in the runtime sweeper to delete offline runtimes
  with no active agents after 7 days.

Closes MUL-695

* fix(daemon): address code review issues on PR #906

1. Move gcRuntimes() to the main sweep loop — previously it was inside
   sweepStaleRuntimes() after an early return, so it only ran when new
   runtimes were marked stale. Now it runs every sweep cycle independently.

2. Fix DeleteStaleOfflineRuntimes to exclude runtimes with ANY agent
   reference (not just active ones). The FK agent.runtime_id is ON DELETE
   RESTRICT, so archived agents also block deletion.

3. Scope MigrateAgentsToRuntime to the same machine by matching
   daemon_id LIKE '<current_daemon_id>-%'. This prevents cross-machine
   agent migration when the same user has multiple devices.

* fix(daemon): use runtime's owner_id for agent migration, not caller's

The migration was gated on ownerID.Valid which is only true for PAT/JWT
registrations. Daemon token registrations (the common case for background
daemon restarts) had ownerID as zero, skipping migration entirely.

Fix: use registered.OwnerID (preserved via COALESCE on upsert) instead
of the caller's ownerID. This ensures migration runs even when the daemon
re-registers via daemon token after an upgrade.
2026-04-14 13:42:27 +08:00
Bohan Jiang
f2efd4b529 Merge pull request #942 from multica-ai/agent/j/e9dce818
fix(cli): fix Windows login requiring two attempts
2026-04-14 13:19:46 +08:00
Jiang Bohan
a1de20e971 fix(cli): fix Windows login requiring two attempts
On Windows, `cmd /c start <url>` treats `&` in the URL as a shell
command separator, truncating the login URL at the first `&cli_state=`
parameter. This causes the OAuth state validation to fail silently,
requiring users to login a second time.

Adding an empty title argument (`""`) before the URL is the standard
Windows fix — `start` interprets the first quoted argument as a window
title, so without it, URLs containing special characters get mangled.
2026-04-14 13:10:28 +08:00
Jiayuan Zhang
27d0865f5f Merge pull request #920 from sanjay3290/fix/gemini-timeout-status
fix(daemon): correct Gemini backend status on timeout and cancellation
2026-04-14 13:03:17 +08:00
Bohan Jiang
2cd6024851 Merge pull request #820 from zoharbabin/feat/local-storage-and-stdin
feat(cli): add --content-stdin flag to issue comment add
2026-04-14 13:02:01 +08:00
Bohan Jiang
5e74c411dc fix(server): cancel active tasks when issue status changes to cancelled (#940)
When a user cancels an issue, active agent tasks now get cancelled
automatically. Previously, task cancellation only triggered on assignee
changes — the cancelled status was incorrectly treated like any other
agent-managed status transition.

Closes #926
2026-04-14 12:53:45 +08:00
Lyon Liang
418049856f merge: resolve conflicts with upstream/main 2026-04-14 12:49:40 +08:00
Naiyuan Qing
00042c0ec7 Merge pull request #932 from multica-ai/NevilleQingNY/weekly-commit-analysis
fix(editor): rewrite bubble menu, link handling, and link preview cards
2026-04-14 12:03:17 +08:00
devv-eve
7c7d7feed3 fix(storage): scope S3 upload keys by workspace (#936)
* fix(storage): scope S3 upload keys by workspace

Upload keys now use `workspaces/{workspace_id}/{uuid}.{ext}` instead of
flat `{uuid}.{ext}`, isolating file storage per workspace. Files uploaded
without workspace context (e.g. avatars) keep the flat key structure.

Refs: MUL-577

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

* fix(storage): scope user uploads under users/{user_id}/ prefix

Non-workspace uploads (avatars, profile images) now use
`users/{user_id}/{uuid}.{ext}` instead of flat `{uuid}.{ext}`,
matching the workspace-scoped pattern from the previous commit.

Refs: MUL-577

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

* fix(storage): fix LocalStorage for nested key paths

- Add MkdirAll before WriteFile to create intermediate directories
  for workspace/user-scoped keys
- Fix KeyFromURL to preserve full path after /uploads/ prefix instead
  of stripping to just the filename
- Update tests to match new behavior

Refs: MUL-577

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

* fix(upload): validate ownership before writing to storage

Move Storage.Upload after issue_id/comment_id ownership validation
to prevent orphaned files in S3 when validation fails. Previously,
the file was uploaded first and validation happened after, leaving
files in workspace-scoped S3 prefixes even on rejected requests.

Refs: MUL-577

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

* fix(upload): restore workspace membership check before upload

The membership check was accidentally removed during the upload
reordering refactor. Without it, any authenticated user could upload
files to any workspace by setting the X-Workspace-ID header.

Also restores the comment explaining the 200-on-DB-error behavior.

Refs: MUL-577

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-13 21:01:50 -07:00
Naiyuan Qing
6a451c1ce7 fix(editor): rewrite bubble menu and link preview with useFloating
Replace Tiptap's BubbleMenu plugin with @floating-ui/react-dom for
all floating editor UI (formatting toolbar, link preview cards).

Architecture:
- useFloating({ strategy:"fixed" }) + createPortal(body) escapes
  all overflow:hidden ancestors (Card component, scroll containers)
- autoUpdate + contextElement monitors all scroll ancestors for
  repositioning; manual update() on transaction for virtual ref changes
- open prop resets isPositioned on visibility change (no stale-position
  flash at 0,0)
- display:none for hiding (not return null which causes blur/focus
  cycle, not visibility:hidden which leaves transition artifacts)
- No blur listener — portal DOM updates cause false editor blurs;
  outside-click + scroll + resize + Escape handle all close cases

Bug fixes:
- BubbleMenu: remove all custom visibility hacks, let selection state
  drive show/hide
- Link preview: new shared card (Copy + Open) for editable editor and
  readonly markdown, portaled to body with fixed positioning
- TitleEditor: use JSON content format (not HTML interpolation that
  loses < > characters)
- Blob URLs: strip from getMarkdown output during upload
- Markdown paste: check clipboard.files first to avoid intercepting
  file paste events
- FileCard: escape HTML attributes in preprocessing
- Link extension: enable linkOnPaste, set defaultProtocol to https,
  switch URL normalization to protocol blocklist (only block
  javascript:/data:/vbscript:)

Dependencies: add @floating-ui/react-dom, remove @tiptap/extension-bubble-menu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:00:17 +08:00
devv-eve
8c0708bb5d fix(server): validate workspace membership for subscriptions and uploads (#935)
* fix(server): validate workspace membership for subscription targets and file uploads

Closes MED-1 (cross-workspace subscription injection) and MED-2 (file upload
missing workspace member validation) from the security audit.

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

* test(server): add negative tests for cross-workspace subscription and upload

Address PR review feedback:
- Add tests verifying cross-workspace user_id is rejected with 403 on
  subscribe and unsubscribe
- Add test verifying upload with foreign workspace_id is rejected with 403
- Make isWorkspaceEntity explicitly enumerate "member"/"agent" and reject
  unknown user types

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-13 20:22:03 -07:00
Lyon Liang
9170b01739 fix(views): focus description editor when clicking empty area 2026-04-14 11:04:58 +08:00
Zohar Babin
d37595b85e fix(cli): address review feedback on --content-stdin flag
- Make --content and --content-stdin mutually exclusive with explicit error
- Use TrimSuffix instead of TrimRight to only strip the trailing newline
- Return "stdin content is empty" instead of misleading "required" error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:14:55 -04:00
Jiayuan Zhang
03310a581a doc: document Homebrew CLI installation (#921) 2026-04-14 05:31:10 +08:00
Sanjay Ramadugu
fe0d450471 fix(daemon): correct Gemini backend status on timeout and cancellation
Check runCtx.Err() before readErr/waitErr so that context-driven
process kills (timeout, user cancellation) report the correct status
("timeout" or "aborted") instead of "failed".

When exec.CommandContext kills the gemini process, io.ReadAll can
return a non-nil error as a side-effect of the closed pipe. The
previous code checked readErr first, masking the real cause. This
aligns gemini.go with the ordering already used in claude.go and
hermes.go.

Fixes #914
2026-04-13 16:30:28 -04:00
Jiayuan Zhang
bc1185f525 Merge pull request #755 from sanjay3290/feat/gemini-backend
feat(daemon): add Google Gemini CLI backend
2026-04-14 02:46:20 +08:00
Bohan Jiang
0d95a7c7ef fix(auth): increase email verification code resend cooldown to 60s (#912)
The 10-second cooldown was too short. Increase to 60 seconds in both
frontend countdown timer and backend rate limit.
2026-04-14 02:35:34 +08:00
KimSeongJun
8587243ab6 web: stabilize login and dashboard redirects (#900) 2026-04-14 02:26:24 +08:00
Bohan Jiang
740d8e773d Merge pull request #841 from multica-ai/agent/j/7bb4859f
feat(desktop): version detection + one-click update
2026-04-14 02:19:25 +08:00
Jiang Bohan
9550e6c4e0 fix(desktop): prevent double-click download and fix dismiss behavior
- Guard handleDownload to only trigger from "available" state
- Only allow dismiss when update is available, not during download/ready
- Use shadcn design tokens (text-success) instead of hardcoded colors
2026-04-14 02:18:55 +08:00
Jiayuan Zhang
880c614039 Merge pull request #905 from multica-ai/agent/emacs/3a193323
fix(issue): inherit parent project for sub-issues
2026-04-14 02:06:26 +08:00
Jiayuan Zhang
f1f693afa5 fix(cli): redirect to web onboarding when new user has no workspaces (#903)
* fix(cli): auto-create workspace for new users during setup

When a new user runs `multica setup` and has no workspaces,
the onboarding flow now auto-creates a default workspace
(named "<name>'s Workspace") instead of failing when the
daemon tries to start with zero watched workspaces.

As a safety net, setup commands also skip daemon start
gracefully if no workspaces are configured, instead of
erroring out.

* fix(cli): redirect to web onboarding instead of auto-creating workspace

When no workspaces exist, the CLI now opens the web onboarding wizard
in the browser and polls until the user completes workspace creation.
This reuses the existing 4-step onboarding flow (workspace → runtime →
agent → done) instead of duplicating creation logic in the CLI.

* fix(cli): address code review — token login crash and misleading success msg

1. Token login (`multica login --token`) on a fresh account no longer
   crashes: waitForOnboarding uses tryResolveAppURL (returns "" instead
   of os.Exit(1)) and falls back to printing manual instructions.

2. Setup commands no longer print "✓ Setup complete!" when onboarding
   was not finished. Shows "⚠ Setup incomplete" with next steps instead.
2026-04-14 02:04:53 +08:00
Jiang Bohan
c148288d5a merge: resolve conflicts with main (deep linking + auto-updater)
Integrate deep link protocol handling, desktopAPI, and auth token flow
from main alongside the auto-updater feature.
2026-04-14 02:02:00 +08:00
Jiayuan Zhang
ff5f6ac2ee fix(daemon): prevent duplicate runtime registration on profile switch (#906)
* fix(daemon): prevent duplicate runtime registration on profile switch

The daemon_id included a profile name suffix (e.g. "hostname-staging"),
so switching profiles created a new daemon_id that bypassed the UPSERT
dedup constraint, leaving orphaned runtime records in the database.

Three changes:
- Remove profile suffix from daemon_id — use stable hostname only.
  The unique constraint (workspace_id, daemon_id, provider) already
  prevents collisions within the same workspace.
- Auto-migrate agents from old offline runtimes to the newly registered
  runtime during DaemonRegister (same workspace/provider/owner).
- Add TTL-based GC in the runtime sweeper to delete offline runtimes
  with no active agents after 7 days.

Closes MUL-695

* fix(daemon): address code review issues on PR #906

1. Move gcRuntimes() to the main sweep loop — previously it was inside
   sweepStaleRuntimes() after an early return, so it only ran when new
   runtimes were marked stale. Now it runs every sweep cycle independently.

2. Fix DeleteStaleOfflineRuntimes to exclude runtimes with ANY agent
   reference (not just active ones). The FK agent.runtime_id is ON DELETE
   RESTRICT, so archived agents also block deletion.

3. Scope MigrateAgentsToRuntime to the same machine by matching
   daemon_id LIKE '<current_daemon_id>-%'. This prevents cross-machine
   agent migration when the same user has multiple devices.
2026-04-14 01:52:34 +08:00
Jiang Bohan
a0d43ca31a feat(agent): OpenClaw backend P0+P1 improvements
Combined P0 and P1 improvements to the OpenClaw agent backend, informed
by PaperClip's adapter architecture:

P0 — User experience:
- Streaming output — emit MessageText as NDJSON events arrive in real
  time, instead of waiting for the final result blob
- Tool use support — parse and emit MessageToolUse/MessageToolResult
  from streaming events, matching Claude and OpenCode backends
- Model & system prompt — pass --model and --system-prompt to the
  OpenClaw CLI when configured

P1 — Robustness:
- Hardened JSON parsing — tryParseOpenclawResult requires lines to
  start with '{', eliminating fragile brace-scanning that could
  false-match JSON fragments in log lines
- Lifecycle event handling — new "lifecycle" event type with phase
  tracking (error/failed/cancelled), plus structured error objects
  (error.name, error.data.message) matching PaperClip's pattern
- Usage field name variants — parseOpenclawUsage supports multiple
  naming conventions (input/inputTokens/input_tokens, cacheRead/
  cachedInputTokens/cache_read_input_tokens, etc.) with incremental
  accumulation across step_finish events

Backwards compatible with the legacy single JSON blob format.
31 tests covering all new functionality.

Closes MUL-726
2026-04-14 01:52:03 +08:00
Jiayuan Zhang
a29ecfe02a test(issue): cover explicit sub-issue project 2026-04-14 01:51:48 +08:00
Jiayuan Zhang
8d3cb21c03 fix(auth): detect cookie-based session during CLI setup flow (#904)
* fix(auth): detect cookie-based session during CLI setup flow

When users run `multica setup` after logging into multica.ai, the CLI
redirects to the login page which only checked localStorage for an
existing session. Since the web app stores auth tokens as HttpOnly
cookies (not localStorage), the session was never detected and users
had to log in again.

Now the login page also tries `api.getMe()` (which sends the HttpOnly
cookie automatically) when no localStorage token exists. A new
`POST /api/cli-token` endpoint lets cookie-authenticated sessions
obtain a bearer token to hand off to the CLI.

* fix(auth): prioritise cookie auth over localStorage in CLI setup flow

Address code review feedback: cookie-first detection avoids authorising
the CLI with a stale or mismatched localStorage token. The useEffect now
calls getMe() without a bearer token first (relying on the HttpOnly
cookie), and only falls back to localStorage if cookie auth fails.

handleCliAuthorize uses an authSourceRef to pick the matching token
source — issueCliToken for cookie sessions, localStorage for token
sessions — preventing the click handler from re-reading a potentially
stale localStorage entry.
2026-04-14 01:46:14 +08:00
Bohan Jiang
2b16cbb27a Merge pull request #908 from multica-ai/agent/j/8cd32f71
fix(selfhost): auto-derive WebSocket URL for LAN access
2026-04-14 01:45:01 +08:00
Jiang Bohan
a757f3a8c4 fix(selfhost): auto-derive WebSocket URL for LAN access (#896)
When NEXT_PUBLIC_WS_URL is not set, the WebSocket URL defaulted to
ws://localhost:8080/ws. This broke real-time features (chat streaming,
live updates, notifications) for self-hosted deployments accessed over
LAN — the browser tried connecting to localhost on the client machine
instead of the Docker host.

Now the web app derives the WebSocket URL from window.location, routing
through the existing Next.js /ws rewrite. This works for localhost, LAN,
and custom domain setups without any extra configuration.

Also adds NEXT_PUBLIC_WS_URL as a Docker build arg for explicit override,
and documents LAN access configuration in SELF_HOSTING_ADVANCED.md.

Closes #896
2026-04-14 01:42:42 +08:00
Jiayuan Zhang
56c38dc521 fix(issue): inherit parent project for sub-issues 2026-04-14 01:30:40 +08:00
Jiayuan Zhang
4bc9969765 fix(scripts): use fully qualified brew package name in install.sh (#901)
BREW_PACKAGE="multica-ai/tap/multica" was defined but never used.
All brew install/upgrade/list commands used the bare name "multica",
which could fail to resolve the correct tap formula. Replace all
occurrences with "$BREW_PACKAGE" to match the Go CLI (update.go)
and Makefile behavior.
2026-04-14 01:19:45 +08:00
Jiayuan Zhang
5b4ee7c5e1 fix(workspace): surface slug conflicts (#895) 2026-04-14 00:09:12 +08:00
Bohan Jiang
b2b909a90f Merge pull request #894 from multica-ai/agent/j/4ae97f0b
revert: handle control_request messages in claude backend (#811)
2026-04-13 23:16:03 +08:00
Jiang Bohan
bf5395f9ee Revert "fix: handle control_request messages in claude backend (auto-approve was dead code) (#811)"
This reverts commit 4d31b1ecee.
2026-04-13 23:11:36 +08:00
Jiayuan Zhang
cd92aad9e1 fix(workspace): auto-retry slug conflicts and show editable URL field (#892)
Workspace creation with duplicate slugs now auto-appends -2, -3, … on
the server side instead of returning 409. The onboarding wizard also
shows an editable Workspace URL field (multica.ai/<slug>) that
auto-generates from the name but can be manually customized.
2026-04-13 23:08:11 +08:00
Bohan Jiang
017f69c123 Merge pull request #881 from tabtablabs-dev/fix/claude-runtime-ping-exit
fix(agent): close Claude stdin after final stream-json result
2026-04-13 22:57:44 +08:00
Bohan Jiang
1e9266f063 fix(install): remove non-existent scoop bucket from Windows installer (#890)
The Install-CliScoop function referenced multica-ai/scoop-bucket.git
which does not exist, causing errors for Windows users with Scoop
installed. Always use direct binary download instead.

Closes #880
2026-04-13 22:53:06 +08:00
Bohan Jiang
1d71df8622 fix(daemon): include dispatched agent identity in CLAUDE.md (#877)
When an agent is triggered via @mention (not as the issue assignee),
the generated CLAUDE.md had no explicit agent identity. The agent would
infer its identity from the issue's assignee field, causing it to skip
work intended for it.

Now CLAUDE.md always includes "You are: <agent-name> (ID: <agent-id>)"
so the agent knows exactly who it is regardless of the issue assignee.

Closes MUL-709
2026-04-13 22:46:36 +08:00
Jiayuan Zhang
576f20f2c7 refactor(cli): separate install from setup, redesign CLI configuration flow (#888)
Decouple install.sh from environment configuration — install.sh now only
installs the CLI binary (and optionally Docker via --with-server), while
all environment configuration moves to `multica setup` subcommands.

Key changes:
- install.sh: remove config writes, rename --local to --with-server
- multica setup: add cloud/self-host subcommands with --server-url,
  --app-url, --port, --frontend-port flags and --profile support
- Add config overwrite protection with interactive prompt
- Remove redundant commands: `config local`, `auth login` alias
- Replace silent multica.ai fallbacks with explicit errors
- Onboarding wizard: dynamically show correct setup command for
  Cloud vs Self-host environments
- Update all docs, landing page, and install scripts for consistency
2026-04-13 22:32:10 +08:00
james
e01fa6bd9e fix(agent): prevent Claude runtime pings from hanging after the model has already finished
Claude's stream-json flow can emit the terminal result event while the
child process still waits on open stdin. Closing stdin as soon as the
final result arrives lets the CLI exit cleanly instead of idling until
the daemon timeout fires.

Constraint: Must preserve the existing Claude stream-json protocol and child-process lifecycle
Rejected: Increase ping timeout only | masks the hang without fixing process exit
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Claude stdin handling aligned with the stream-json terminal result semantics; do not defer closure until goroutine teardown
Tested: Reproduced self-hosted runtime ping timeout locally; verified ping succeeds after closing stdin on result; cd server && go test ./pkg/agent
Not-tested: Full make check; Bedrock/Vertex-specific Claude auth flows
2026-04-13 08:59:34 -05:00
Jiayuan Zhang
f1236b2358 fix(chat): remove archive functionality from chat session history (#879)
Remove the archive button, active/archived grouping, and related
imports from ChatSessionHistory. This also fixes the nested <button>
hydration error (GitHub #875) since the inner archive Button was the
only nested button inside the row's outer <button>.
2026-04-13 21:31:53 +08:00
Bohan Jiang
0b60f78e8a fix(comment): set trigger_comment_id to actual reply, not thread root (#871)
* fix(comment): set trigger_comment_id to actual reply, not thread root

When a user replies in a thread and @mentions an agent, the enqueued
task's trigger_comment_id was incorrectly set to the parent (thread
root) comment instead of the reply that contained the mention. This
caused the agent to read the wrong comment and miss the user's actual
instructions.

Always pass comment.ID to EnqueueTaskForMention so agents see the
comment that triggered them.

Fixes MUL-708

* fix(task): resolve thread root in createAgentComment for reply triggers

With trigger_comment_id now correctly pointing to the actual reply
(not the thread root), createAgentComment must resolve to the thread
root before posting. Otherwise error/system comments would have
parent_id pointing to a nested reply, making them invisible in the
frontend's flat thread grouping.

Part of MUL-708
2026-04-13 19:53:23 +08:00
leaderlemon
5cd58183b2 fix(openclaw): handle JSON results with durationMs but no payloads (#862)
Some OpenClaw JSON outputs contain durationMs but lack payloads field.
The original condition rejected these results, causing the agent to
return "openclaw returned no parseable output" instead of the actual
execution result.

Fix by accepting results that have either payloads OR durationMs > 0.

Fixes #830

Co-authored-by: leaderlemon <leaderlemon@users.noreply.github.com>
2026-04-13 19:41:47 +08:00
Naiyuan Qing
83ff80c3ed Merge pull request #869 from multica-ai/NevilleQingNY/editor-audit
feat(editor): add bubble menu for text formatting
2026-04-13 19:27:16 +08:00
Bohan Jiang
8fb3bd322e fix(auth): AuthInitializer not supporting cookie auth mode (#870)
AuthInitializer only checked for multica_token in localStorage. In
cookie auth mode (introduced by the HttpOnly cookie migration), there
is no localStorage token — so AuthInitializer immediately set the user
to null and triggered a logout redirect on every page load/reload.

Add a cookieAuth code path that calls api.getMe() using the HttpOnly
cookie sent automatically by the browser, matching the auth store's
initialize() logic.

Fixes MUL-705, fixes #864
2026-04-13 19:25:49 +08:00
Naiyuan Qing
06b1b99638 fix(editor): use w-auto for bubble menu dropdown widths
Prevents text wrapping in heading/list dropdown items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:23:05 +08:00
Naiyuan Qing
156982dc83 fix(editor): use onClick instead of onSelect for dropdown menu items
base-ui's Menu.Item only supports onClick, not onSelect (which is a
Radix UI API). onSelect was being silently ignored, causing heading
and list dropdown actions to never execute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:21:41 +08:00
Naiyuan Qing
b239aa383e fix(editor): bubble menu dropdown/blur/scroll interactions
- Track child dropdown open state via ref to prevent blur handler from
  hiding the menu while a heading/list dropdown is open
- Hide on ancestor scroll only (not sidebar/dropdown scroll)
- Hoist BubbleMenu options to module constant to avoid excessive plugin
  updateOptions dispatches on every render
- Recover bubble menu after scroll via selectionUpdate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:18:35 +08:00
Bohan Jiang
e2e5de1b26 docs: add v0.1.28 changelog entry (2026-04-13) (#867) 2026-04-13 19:13:53 +08:00
Naiyuan Qing
0faf1363ee feat(editor): add bubble menu for text formatting
Add a floating toolbar that appears when text is selected in the editor.
Supports inline marks (bold/italic/strike/code), link editing with URL
auto-prefix, heading/list dropdowns, and blockquote toggle. Uses Tiptap's
BubbleMenu with fixed positioning and z-50 to escape overflow containers.
Hides on editor blur and ancestor scroll, recovers on new selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:13:38 +08:00
devv-eve
6c92108b09 fix: replace hardcoded Unix path separators with filepath.Join and os.TempDir (#860)
- cmd_daemon.go: use filepath.Join for PID/log file paths instead of string concat with "/"
- codex_home.go: use os.TempDir() instead of hardcoded "/tmp" for cross-platform fallback

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 04:11:51 -07:00
Bohan Jiang
a94c6481dd fix: compute sub-issue progress from database instead of paginated client cache (#865)
The sub-issue progress indicator (e.g. "0/2") was undercounting because
it was computed from the client-side issue list, which only loads the
first 50 done issues. Sub-issues marked as done beyond that page were
excluded from both the total and done counts.

Added a dedicated backend endpoint (GET /api/issues/child-progress) that
aggregates child issue counts directly from the database, ensuring
accurate totals regardless of client-side pagination or filtering.

Fixes MUL-702
2026-04-13 19:10:28 +08:00
Naiyuan Qing
b4de4c9e9f Merge pull request #861 from multica-ai/feat/chat-ui-improvements
feat(chat): overhaul chat UI — resize, animations, session history
2026-04-13 18:31:00 +08:00
Bohan Jiang
7cac8014c9 feat(views): add keyboard navigation to assignee picker (#857)
* feat(views): add keyboard navigation and auto-select to PropertyPicker

Add arrow key (up/down) navigation and Enter key selection to the
searchable PropertyPicker dropdown. When the search narrows results to
a single match, pressing Enter auto-selects it without needing to
arrow-navigate first. Fixes GitHub issue #793.

* fix(views): hide Unassigned option when search filter is active

When the user types a search query in the assignee picker, the
Unassigned option is no longer pinned at the top — it only shows
when there is no active filter.

* feat(views): auto-highlight first result when searching in picker
2026-04-13 18:30:48 +08:00
Naiyuan Qing
be8b099c12 feat(desktop): add remote API proxy mode for dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:27:43 +08:00
Naiyuan Qing
458b1e19e2 feat(chat): improve session history UX and align chat window offset
- Add optimistic update + rollback to archive mutation
- Replace Trash2 with Archive icon (correct semantics)
- Add Tooltip on archive button, replace native title
- Show spinner during archive, toast on error
- Use cn() for className composition
- Align chat window offset to bottom-2 right-2 (match FAB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:27:38 +08:00
Naiyuan Qing
acad93163b feat(chat): replace native title with Tooltip on chat header buttons
Use the project's Tooltip component instead of native title attributes
for consistent styling, animation, and accessibility across the app.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:24:01 +08:00
LinYushen
526e336081 feat(execenv): add Windows fallback for symlink operations (#859)
On Windows, os.Symlink requires Developer Mode or admin privileges.
Extract symlink creation into platform-specific files: on non-Windows,
behavior is unchanged (os.Symlink). On Windows, try os.Symlink first,
then fall back to directory junctions (mklink /J) for dirs and file
copy for files.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:23:41 +08:00
Naiyuan Qing
f4ce4c249d chore: remove redundant icon size classes on pin buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:21:44 +08:00
LinYushen
69f8380b9c Merge pull request #855 from multica-ai/agent/cc-girl/08bf694e
refactor(daemon): separate Unix/Windows platform code (MUL-690)
2026-04-13 18:20:39 +08:00
Naiyuan Qing
2e5af72cdc feat(chat): resizable chat window with animations and improved UX
- Refactor store to persist raw user intent (chatWidth/chatHeight/isExpanded) with no clamp logic
- Add ResizeObserver-based resize hook for dynamic container tracking
- Add drag-to-resize handles (left, top, corner) with pointer capture
- Expand/Restore button uses visual state (isAtMax) not internal flag
- Open/close animation (scale + opacity from bottom-right)
- Resize animation on button click, instant on drag (isDragging gate)
- Move ChatWindow inside content area (absolute, not fixed)
- Add input draft persistence, remove agent prop from message list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 18:20:13 +08:00
yushen
0a0a86da2c fix(daemon): restore HideWindow: true for Windows daemon child process
Prevents console window flash when starting daemon in background on
Windows. This field existed in the original sysproc_windows.go but was
lost during merge conflict resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:17:55 +08:00
yushen
96e87f7200 merge: resolve main conflicts, consolidate platform files
Main introduced sysproc_unix.go/sysproc_windows.go with a simpler
version of the same refactoring. Our cmd_daemon_unix.go/windows.go
files are more comprehensive (reverse-scan tail, graceful CTRL_BREAK
stop, named constants), so we keep ours and remove the overlapping
sysproc_*.go files. Conflict in cmd_daemon.go resolved using our
function names (notifyShutdownContext, stopDaemonProcess).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:15:22 +08:00
yushen
9e7d1eb764 fix(daemon): address Windows nits — named const, reverse-scan tail, graceful stop
1. Extract magic number 0x00000200 to createNewProcessGroup const
2. Replace os.ReadFile with reverse-scan from EOF in tailLogFile to
   avoid loading entire log file into memory
3. Try CTRL_BREAK_EVENT for graceful shutdown before falling back to
   process.Kill(); register sigBreak in notifyShutdownContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:12:01 +08:00
LinYushen
007a1ca284 feat(cli): add Windows installation support (#854)
* feat(cli): add Windows installation support (MUL-689)

Add PowerShell install script and Windows binary builds so Windows users
can install the CLI without WSL.

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

* fix(cli): address PR review for Windows install script

- Use GitHub REST API for Get-LatestVersion (PS 5.1 compatible)
- Add SHA256 checksum verification after download
- Use [System.Version] for proper semantic version comparison
- Refactor $arch assignment for readability
- Warn before git reset --hard in Install-Server

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-13 18:05:22 +08:00
LinYushen
c5fce56887 feat(release): add Windows build target to GoReleaser (#856)
* feat(release): add Windows build target to GoReleaser

Add windows to goos list, use .zip archive format for Windows builds,
and extract platform-specific SysProcAttr into build-tagged files to
fix cross-compilation.

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

* fix(release): Windows daemon signal handling and process group

Add CREATE_NEW_PROCESS_GROUP to Windows SysProcAttr so the daemon child
process can receive CTRL_BREAK_EVENT. Extract signal handling into
platform-specific helpers: Unix uses SIGTERM for graceful stop, Windows
uses os.Interrupt (CTRL_BREAK_EVENT).

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-13 18:05:08 +08:00
Bohan Jiang
04747b45a2 fix(auth): add user-facing task messages endpoint for cookie auth (#853)
The frontend's listTaskMessages() was calling /api/daemon/tasks/{id}/messages
which uses DaemonAuth middleware (requires Authorization header). After the
cookie auth migration (#819), cookie-mode sessions don't send an Authorization
header, causing 401 on this endpoint. The 401 then triggers handleUnauthorized()
which clears the workspace context, cascading into 400 errors on all subsequent
requests.

Fix: add GET /api/tasks/{taskId}/messages under regular user auth middleware,
and update the frontend to use it instead of the daemon endpoint.

Closes #833
2026-04-13 18:03:25 +08:00
Jiayuan Zhang
01232fc2f9 feat(onboarding): add full-screen onboarding wizard for new workspaces (#852)
* feat(onboarding): add full-screen onboarding wizard for new workspaces

Replace auto-provisioned workspace with an interactive 4-step onboarding
wizard: Create Workspace → Connect Runtime → Create Agent → Get Started.

- Remove server-side ensureUserWorkspace() so new users land in onboarding
- Add onboarding wizard in packages/views/onboarding/ (4 steps)
- Wire login/OAuth callbacks to redirect to /onboarding when no workspace
- Add DashboardGuard onboardingPath fallback for workspace-less users
- Sidebar "Create workspace" navigates to /onboarding instead of modal
- Remove CreateWorkspaceModal (replaced by wizard step 1)
- Auto-generate workspace slug from name (no user-facing URL field)
- Unified CLI install flow: install.sh + multica setup (auto-detects local)
- Create onboarding issues on completion with interactive "Say hello" task

* test(auth): update workspace tests to match onboarding flow

Login no longer auto-creates workspaces — new users start with zero
workspaces and create one through the onboarding wizard. Update both
integration and handler tests to assert 0 workspaces after verify-code.
2026-04-13 17:59:51 +08:00
yushen
4372c5f4fa refactor(daemon): separate Unix/Windows platform code with build tags
Extract Unix-only syscalls (Setsid, SIGTERM, tail command) into
cmd_daemon_unix.go and provide Windows alternatives in
cmd_daemon_windows.go using CREATE_NEW_PROCESS_GROUP, process.Kill(),
os.Interrupt, and native Go file reading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:54:11 +08:00
Jiang Bohan
a73a9d4036 fix(agent): address PR review — env var blocklist, unmarshal logging, stable React keys
1. Security: add isBlockedEnvKey() blocklist that rejects MULTICA_*
   prefix and critical system vars (HOME, PATH, USER, SHELL, TERM,
   CODEX_HOME) from custom_env injection
2. Observability: log warnings when json.Unmarshal fails on custom_env
   (agentToResponse + claim endpoint)
3. UX: use stable auto-increment IDs for env entry React keys instead
   of array index to prevent input focus/state issues on add/remove
2026-04-13 17:39:02 +08:00
Bohan Jiang
12bf7cac34 fix(security): WebSocket first-message auth (MUL-580) (#848)
* fix(security): use first-message auth for WebSocket instead of URL query param

Token was exposed in URL query parameters (HIGH-4 from security audit),
visible in server/proxy logs, browser history, and referrer headers.

Now non-cookie clients (desktop, CLI) send the token as the first
WebSocket message after the connection opens. Cookie-based auth (web)
continues to work unchanged. Server-side auth priority flipped to
cookie-first.

Closes MUL-580

* fix(security): add auth_ack and fix test JSON construction

Server sends auth_ack after successful first-message auth so the client
knows auth completed before firing reconnect callbacks. Test now uses
json.Marshal instead of string concatenation for the auth message.

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

* fix(test): update WebSocket integration test for first-message auth

The integration test still passed the token as a URL query param,
causing a timeout since the server now expects first-message auth
for non-cookie clients.

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

---------

Co-authored-by: yushen <ldnvnbl@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:11:52 +08:00
Naiyuan Qing
64ed0806ff refactor(chat): polish chat UI with design system tokens and components
- Replace raw <button> with <Button variant="ghost" size="icon-sm"> in header and history
- Add aria-expanded:bg-accent to agent selector trigger for open state
- Add max-h-60, w-auto max-w-56, truncate to agent dropdown
- Switch FAB to bg-card, chat window to bg-sidebar
- Switch user message bubble from bg-primary to bg-muted, drop text-primary-foreground
- Reduce user bubble max-w from 85% to 80%
- Remove agent avatar from AI messages, make AI content w-full
- Strip arbitrary text-[10px] from AvatarFallback
- Remove manual icon size overrides inside Button components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:57:48 +08:00
Naiyuan Qing
b927684e3d Merge pull request #847 from multica-ai/NevilleQingNY/workspace-create-btn
fix(views): make create workspace button visible
2026-04-13 16:55:01 +08:00
Naiyuan Qing
e9bed4eb13 fix(views): make create workspace button always visible in dropdown
The create workspace button was hidden behind a hover interaction on the
"Workspaces" label, making it very hard to discover. Replace it with a
standard DropdownMenuItem at the bottom of the workspace list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:52:13 +08:00
pradeep7127
297b436e65 fix(issue): default create status to todo instead of backlog (#746)
* fix(issue): default create status to todo instead of backlog

Issues created without an explicit status now default to `todo` so the
local daemon picks them up immediately. Previously they defaulted to
`backlog`, which daemons ignore, leaving new issues silently idle until
a user manually moved them.

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

* test(issue): verify create defaults to todo, explicit backlog still works

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:49:35 +08:00
Jiang Bohan
4165401d16 feat(agent): support custom environment variables for router/proxy mode
Add per-agent custom_env configuration that gets injected into the agent
subprocess at launch time. This enables users to configure custom API
endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud
provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without
requiring code changes.

Changes:
- Migration 040: add custom_env JSONB column to agent table
- Backend: custom_env in agent CRUD API + claim endpoint
- Daemon: merge custom_env into subprocess environment variables
- Frontend: env var editor in agent settings (key-value pairs with
  visibility toggle for sensitive values)

Closes #816
Related: #807, #809
2026-04-13 16:47:56 +08:00
Naiyuan Qing
6097f7392e refactor(chat): migrate chat input to ContentEditor + unified SubmitButton
- Replace plain textarea in chat-input with ContentEditor (rich text, matches comment-input structure)
- Extract shared SubmitButton component (idle/loading/running states) to packages/ui/components/common
- Update comment-input to use icon-sm size
- Fix chat-fab Tooltip delay prop (not supported on Root, global 500ms applies)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:52:39 +08:00
Naiyuan Qing
a749d310dd chore(core): remove ReactQueryDevtools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:52:34 +08:00
LinYushen
a473110078 Merge pull request #839 from multica-ai/agent/cc-girl/84c5483e
feat(daemon): add periodic GC for workspace isolation directories
2026-04-13 15:51:16 +08:00
yushen
2f1000d815 merge: resolve conflict with main (runTask refactor + mergeUsage)
Main introduced executeAndDrain/mergeUsage refactor. Resolve by keeping
main's refactored structure and re-applying EnvRoot to the switch/case
in runTask. Rename newTestDaemon → newGCTestDaemon to avoid collision
with the helper added in daemon_test.go on main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:51:02 +08:00
Bohan Jiang
dbc6308c20 fix(desktop): strip Origin header from WebSocket requests (#842)
The server's WS origin whitelist (added in #819) rejects connections
from localhost dev origins. Desktop app doesn't need Origin-based
security since it runs in Electron with webSecurity disabled.

Strip the Origin header from WS upgrade requests in the main process
so the server's checkOrigin allows the connection.
2026-04-13 15:50:05 +08:00
Naiyuan Qing
9e8c20df3d Merge remote-tracking branch 'origin/main' into feat/chat-ui-improvements 2026-04-13 15:50:01 +08:00
Cocoon-Break
4d31b1ecee fix: handle control_request messages in claude backend (auto-approve was dead code) (#811)
* fix: handle control_request messages in claude backend to enable auto-approve (Closes #810)

Signed-off-by: cocoon <54054995+kuishou68@users.noreply.github.com>

* fix: defer stdin.Close() inside goroutine so control_request writes can succeed

Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>

---------

Signed-off-by: cocoon <54054995+kuishou68@users.noreply.github.com>
Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
2026-04-13 15:47:28 +08:00
Naiyuan Qing
17ea7797df Merge remote-tracking branch 'origin/main' into feat/chat-ui-improvements 2026-04-13 15:38:35 +08:00
Bohan Jiang
418fe4b18e feat(desktop): implement Google login via deep link (#626)
Desktop Google login flow: click "Continue with Google" → opens default
browser to web login page with platform=desktop → Google OAuth completes
→ web callback redirects to multica://auth/callback?token=<jwt> →
Electron receives deep link, extracts token, completes login.

Changes:
- Register `multica://` protocol in Electron (main process + builder)
- Add single-instance lock with deep link forwarding (macOS + Win/Linux)
- Expose `desktopAPI.onAuthToken` and `openExternal` via preload IPC
- Add `loginWithToken(token)` to core auth store
- Pass `state=platform:desktop` through Google OAuth flow
- Web callback detects desktop state and redirects via deep link
- Desktop renderer listens for auth token and hydrates session
2026-04-13 15:33:14 +08:00
Jiang Bohan
e5881601ad feat(desktop): add auto-update with GitHub releases
Check for updates on startup via electron-updater. When a new version is
detected, show a notification in the bottom-right corner with download
and restart-to-install actions.
2026-04-13 15:31:58 +08:00
Roshan Warrier
e044c7e84b fix(agent): parse openclaw result incrementally (#836)
Co-authored-by: txhno <198242577+txhno@users.noreply.github.com>
2026-04-13 15:29:34 +08:00
Bohan Jiang
afab4dfdef Merge pull request #840 from multica-ai/agent/j/9cf0cf3e
fix(daemon): run repo cache sync in background to unblock heartbeat
2026-04-13 15:27:45 +08:00
Jiang Bohan
99e973ba3e fix(daemon): run repo cache sync in background to unblock heartbeat
The repoCache.Sync() call in loadWatchedWorkspaces runs synchronous git
clone/fetch operations that can take minutes for large repos. Because
heartbeatLoop and pollLoop only start after loadWatchedWorkspaces returns,
the runtime's last_seen_at is never updated during the sync, causing the
server's sweeper to mark it offline after 45 seconds.

Move repo cache sync to a background goroutine so heartbeat and poll
loops start immediately after runtime registration.

Closes #825
2026-04-13 15:19:02 +08:00
Bohan Jiang
6ce0ba46a9 Merge pull request #800 from multica-ai/agent/j/ce0987c2
fix(ws): include issue_id in task:dispatch event
2026-04-13 15:08:12 +08:00
Naiyuan Qing
547da4c3e5 refactor(chat): replace pill FAB with circular icon button + tooltip
- Switch from pill shape (px-4 py-2) to 40×40 circle (size-10)
- Replace Send icon with MessageCircle
- Add hover scale animation (scale-110) and active press (scale-95)
- Add Tooltip with side=top, sideOffset=10, delay=300ms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:07:11 +08:00
yushen
14beaa6ce2 fix(daemon): extract pruneWorktree helper for idiomatic defer cancel
The context cancel in pruneRepoWorktrees was called explicitly after
CombinedOutput inside a loop. Extract to a helper method so defer
cancel() works correctly (scoped to the function, not the loop).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:05:59 +08:00
Jiayuan Zhang
a3eefcf2c4 Revert "feat: add online status indicator on agent & member avatars (#821)" (#837)
This reverts commit 1d64ea4ba6.
2026-04-13 15:03:31 +08:00
yushen
20809052f5 fix(daemon): address GC review feedback
- Move WriteGCMeta from runTask() to handleTask() so it runs after
  task completion, not at start. Mid-task crashes leave orphan dirs
  that get cleaned by GCOrphanTTL.
- Strengthen isBareRepo to check both HEAD and objects/ directory.
- Remove empty workspace directories after all task dirs are cleaned.
- Add 30s context timeout to git worktree prune to prevent hangs.
- Add comprehensive unit tests for shouldCleanTaskDir (8 scenarios),
  cleanTaskDir, gcWorkspace empty-dir cleanup, isBareRepo, and
  WriteGCMeta/ReadGCMeta roundtrip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:00:37 +08:00
LinYushen
265d1854c9 fix(daemon): add fallback for failed session resume (#818)
* fix(daemon): add fallback for failed session resume

When the daemon tries to resume a prior session (--resume flag for
Claude, --session for OpenCode, session/resume RPC for Hermes) and the
session no longer exists, the agent fails immediately. This adds a
fallback that retries the execution with a fresh session instead of
marking the task as blocked.

Extracts the execute+drain logic into a reusable executeAndDrain method
to avoid code duplication between the initial attempt and the retry.

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

* fix(daemon): narrow session resume retry and merge usage

Address review feedback:
1. Narrow retry trigger: only retry when result.SessionID == "" (no
   session was established), not on any failure with PriorSessionID set
2. Merge token usage from both attempts so billing is accurate
3. Log errors when the retry itself fails to start
4. Add unit tests for mergeUsage, fallback behavior, and no-retry
   when session was already established

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-13 14:47:24 +08:00
yushen
ff206baa6f feat(daemon): add periodic GC for workspace isolation directories
Isolation directories accumulate indefinitely because they're preserved
for session reuse but never cleaned up after the issue is closed.

This adds a background GC loop that periodically scans local workspace
directories and removes those whose issue is done/canceled and hasn't
been updated for 5 days (configurable via MULTICA_GC_TTL). Orphan
directories with no metadata are cleaned after 30 days.

Changes:
- Write .gc_meta.json (issue_id, workspace_id) at task completion
- Add GET /api/daemon/issues/{issueId}/gc-check endpoint for status queries
- Add gcLoop goroutine to daemon with configurable interval/TTL
- Prune stale git worktree references from bare repo caches each cycle
- New env vars: MULTICA_GC_ENABLED, MULTICA_GC_INTERVAL, MULTICA_GC_TTL,
  MULTICA_GC_ORPHAN_TTL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:46:48 +08:00
Jiayuan Zhang
1d64ea4ba6 feat: add online status indicator on agent & member avatars (#821)
* feat: add online status indicator dot on agent & member avatars

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

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

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

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

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

Same fix as the hub unit tests — read and discard the member:online
event before asserting on issue:created in TestWebSocketIntegration.
2026-04-13 14:46:34 +08:00
LinYushen
c8275605c9 fix(auth): fall back to token-mode WS for legacy localStorage users (#831)
* fix(auth): fall back to token-mode WS for users with legacy localStorage token

Users who logged in before the cookie-auth migration still have multica_token
in localStorage but no multica_auth cookie. Forcing cookieAuth=true for every
session caused their WebSocket upgrade to 401 with only workspace_id in the URL.

Detect the legacy token at boot and run that session in token mode (Bearer HTTP
+ URL-param WS). Pure cookie-mode is used only when no legacy token is present,
so new users get the intended path and legacy users migrate naturally on their
next logout/login cycle (logout already clears multica_token).

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

* docs(auth): note sunset plan for legacy-token WS fallback

Make the XSS-exposure tradeoff explicit and give future maintainers a
concrete signal (<1% of sessions) for when to delete the compat branch.

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-13 14:40:03 +08:00
Bohan Jiang
c54f9a0bc4 Merge pull request #829 from multica-ai/agent/j/03e8009e
fix(daemon): embed triggering comment content in agent prompt
2026-04-13 14:36:21 +08:00
Naiyuan Qing
30725392ac Merge pull request #827 from multica-ai/refactor/workspace-list-to-react-query
refactor(workspace): migrate workspace list from Zustand to React Query
2026-04-13 14:29:10 +08:00
Naiyuan Qing
3f13605b4c test(views/login): mock useQueryClient to fix No QueryClient error
LoginPage now calls useQueryClient() after the workspace list migration.
Mock it in packages/views tests so render calls don't need wrapping in
QueryClientProvider — setQueryData becomes a no-op spy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:25:21 +08:00
Jiang Bohan
93fffad82a fix(daemon): embed triggering comment content in agent prompt
When a task is triggered by a comment, the agent prompt now includes
the comment content directly. This prevents the agent from ignoring
the comment when stale output files exist in a reused workdir.

Closes #805
2026-04-13 14:20:26 +08:00
Naiyuan Qing
2fd344511e fix(realtime): add staleTime: 0 to fetchQuery in WS deleted/removed handlers
workspace:deleted and member:removed handlers were calling fetchQuery
without staleTime: 0. With staleTime: Infinity on the QueryClient, this
returns the cached list (which still contains the deleted/left workspace)
instead of fetching fresh data — so hydrateWorkspace never switches away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:20:19 +08:00
Naiyuan Qing
9581e4d870 test(web/login): wrap render with QueryClientProvider
LoginPage now calls useQueryClient() after the workspace list migration.
All test renders need a QueryClientProvider; add a createWrapper() helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:18:11 +08:00
Jiayuan Zhang
cb4f5071ab fix: update X link to @MulticaAI in readme and landing page (#826)
Replace outdated x.com/multica_hq with x.com/MulticaAI in:
- README.zh-CN.md
- Landing page shared config
- Landing page en/zh i18n files
2026-04-13 14:14:34 +08:00
Naiyuan Qing
c76ba2f58e fix(workspace): seed React Query cache at all list-acquisition points
- staleTime: 0 on fetchQuery after leave/delete so fresh data is fetched
- setQueryData before switchWorkspace in createWorkspace so sidebar is
  consistent on first render
- seed workspaceKeys.list() cache in login, Google callback, and
  settings save so the first useQuery(workspaceListOptions()) hit is free
- remove dead onError from WorkspaceStoreOptions (used only by the
  deleted refreshWorkspaces action)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:00:28 +08:00
Bohan Jiang
bec84e2013 Merge pull request #824 from multica-ai/agent/j/642cc7b4
feat(daemon): add token usage log scanning for OpenCode, OpenClaw, Hermes
2026-04-13 13:50:41 +08:00
Jiang Bohan
2ea778796a feat(daemon): add token usage log scanning for OpenCode, OpenClaw, and Hermes runtimes
Previously only Claude and Codex had log-scanning-level token usage
reporting (Flow B). This adds scanners for the remaining three runtimes:

- OpenCode: reads JSON message files from ~/.local/share/opencode/storage/message/
- OpenClaw: reads JSONL session files from ~/.openclaw/agents/*/sessions/
- Hermes: reads JSONL session files from ~/.hermes/sessions/

All three are registered in Scanner.Scan() and follow the same
(date, provider, model) aggregation pattern as existing scanners.
2026-04-13 13:42:05 +08:00
Naiyuan Qing
43466a6402 refactor: migrate workspace list from Zustand to React Query
- Remove workspaces[] from workspace store — list is server state, belongs in React Query
- Change switchWorkspace(id) → switchWorkspace(ws) — caller provides full object from Query
- Remove createWorkspace/leaveWorkspace/deleteWorkspace store actions (duplicated mutations)
- Remove refreshWorkspaces store action — replaced by qc.fetchQuery + hydrateWorkspace
- Enhance useLeaveWorkspace/useDeleteWorkspace mutations to re-select workspace when current is removed
- useCreateWorkspace mutation now switches to new workspace on success
- AuthInitializer seeds React Query cache on boot to avoid double fetch
- Realtime sync: replace refreshWorkspaces() calls with qc.fetchQuery + hydrateWorkspace
- Sidebar reads workspace list from useQuery(workspaceListOptions()) instead of Zustand
- create-workspace modal and workspace settings tab use mutations directly
- AGENTS.md: rewrite to match current monorepo architecture, pointing to CLAUDE.md

Fixes workspace rename not updating sidebar without page refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:38:02 +08:00
Bohan Jiang
68b101fe01 Merge pull request #804 from igornumeriano/fix/x-link
fix: update X link to correct handle @MulticaAI
2026-04-13 13:11:34 +08:00
LinYushen
e20c507dcc fix(security): add Content-Security-Policy response header (#822)
Adds CSP middleware to the global middleware chain as a browser-level
defense against XSS: script-src 'self', object-src 'none',
frame-ancestors 'none', base-uri 'self', form-action 'self'.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:53:39 +08:00
Zohar Babin
77dbcaefad feat(cli): add --content-stdin flag to issue comment add
Allow agents to pipe comment content through stdin instead of the
--content flag, avoiding shell escaping issues with backticks, quotes,
and other special characters in markdown content.

Usage: cat <<'COMMENT' | multica issue comment add <id> --content-stdin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 00:23:16 -04:00
LinYushen
95bfd7dd96 feat(auth): migrate auth token to HttpOnly Cookie & WebSocket Origin whitelist (#819)
* feat(auth): migrate auth token to HttpOnly cookie & implement WebSocket Origin whitelist

Security improvements from the MUL-566 audit report:

1. Auth token is now set as an HttpOnly, SameSite=Lax cookie on login,
   preventing XSS-based token theft. Cookie-based auth includes CSRF
   protection via double-submit cookie pattern. The Authorization header
   path is preserved for Electron desktop app and CLI/PAT clients.

2. WebSocket upgrader now validates the Origin header against a
   configurable allowlist (ALLOWED_ORIGINS env var), rejecting
   connections from unauthorized origins.

Backend: new auth cookie helpers, middleware reads cookie as fallback,
WS handler accepts cookie auth, Origin whitelist, logout endpoint.
Frontend: CSRF token in API headers, cookie-aware auth store and WS
client, web app opts into cookieAuth mode while desktop keeps tokens.

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

* fix(auth): address PR review — Strict cookies, HMAC-bound CSRF, origin sync

1. SameSite=Lax → SameSite=Strict per spec requirement
2. CSRF token now HMAC-signed with auth token (nonce.signature format),
   preventing subdomain cookie injection attacks
3. allowedWSOrigins uses atomic.Value to eliminate data race
4. Removed magic "cookie" sentinel string in WSProvider — pass null token
   and guard with boolean check instead
5. Removed dead delete uploadHeaders["Content-Type"] in API client

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-13 12:13:35 +08:00
Igor Numeriano
3bf7f467a2 fix: update X (Twitter) link to correct handle @MulticaAI
The previous link pointed to https://x.com/multica_hq which returns
a 404. The correct handle is @MulticaAI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:38:27 -03:00
Jiayuan Zhang
04238bea22 chore: simplify v0.1.27 changelog — merge related items, remove trivial entries (#803) 2026-04-13 02:58:22 +08:00
Bohan Jiang
c13d365015 Merge pull request #796 from bulai0408/fix/codex-sandbox-network-access
fix(agent): enable network access for Codex sandbox
2026-04-13 01:55:12 +08:00
Jiang Bohan
b271e8915e fix(ws): include issue_id in task:dispatch event to prevent cross-issue UI glitch
broadcastTaskDispatch was the only task event broadcast missing issue_id
in its WebSocket payload. The frontend task:dispatch handler had no way
to filter by issue, causing AgentLiveCard to briefly show activity for
the wrong issue when multiple tabs are open.

Closes https://github.com/multica-ai/multica/issues/791
2026-04-13 01:50:10 +08:00
bulai0408
47eb6cb612 fix(agent): enable network access for Codex sandbox so Multica CLI can reach API
Codex tasks running in workspace-write sandbox mode could not resolve
api.multica.ai because the hardcoded sandbox parameter in thread/start
overrode any config.toml settings, and the default sandbox policy blocks
network access.

Changes:
- Remove hardcoded `sandbox: "workspace-write"` from thread/start RPC —
  let Codex read sandbox config from its own config.toml instead
- Auto-generate config.toml in per-task CODEX_HOME with
  `sandbox_mode = "workspace-write"` and `network_access = true`,
  preserving any existing user settings
- Fix Reuse() to restore CodexHome for Codex provider on workdir reuse

Closes #368
2026-04-13 01:03:43 +08:00
Bohan Jiang
1ee4e0501a fix(handler): add .claude/skills/ candidate path for skills.sh import (#792)
Skills stored under .claude/skills/{name}/SKILL.md (the Claude Code
native discovery convention) were not found during skills.sh import,
causing a 502 error. Add this path to the candidate list.

Fixes #777
2026-04-12 23:39:46 +08:00
Bohan Jiang
544b9bc971 docs: add v0.1.27 changelog entry (2026-04-12) (#790) 2026-04-12 23:35:48 +08:00
tianrking
0c19f0d16f Fix workspace filter sync and align CLI docs (#722)
* Fix workspace filter sync and align CLI docs

* simplify workspace sync subscription in issues page

* docs(self-hosting): align supported agents and daemon env vars
2026-04-12 23:11:40 +08:00
Bohan Jiang
d74d7f2b7b fix(handler): add cycle detection to BatchUpdateIssues parent_issue_id handling (#788)
BatchUpdateIssues was missing the ancestor-walk cycle detection that
single UpdateIssue has. This allowed creating circular parent
relationships (e.g. A→B→A) via the batch API. Added the same
depth-limited walk (up to 10 ancestors) to detect and skip issues
that would create cycles, consistent with UpdateIssue behavior.
2026-04-12 23:03:46 +08:00
Qiaochu Hu
0c2102b951 fix(handler): fix batch operations and error handling bugs (#779)
fix(handler): fix batch operations and error handling bugs
2026-04-12 23:00:40 +08:00
zerion-925
0c28d3cd08 fix: add randomUUID fallback for non-secure contexts (#749)
* fix: fallback when crypto.randomUUID is unavailable

* fix(core): remove Math.random UUID fallback and add tests

---------

Co-authored-by: Zerion <dev@take-app.local>
2026-04-12 22:51:56 +08:00
Jiang Bohan
7312b5650c fix(server): fix ListRuntimeUsage to filter by date range instead of row count (#765)
Replace LIMIT $2 with AND date >= $2 in ListRuntimeUsage query. When a
runtime uses multiple models each day has multiple rows, so a row LIMIT
silently returns fewer days than requested.

Also fixes displayName warnings in issue-detail test mocks and adds
missing setOpen to useCallback deps in search-command.

Co-authored-by: jayavibhavnk <jaya11vibhav@gmail.com>
Closes #731
2026-04-12 22:46:07 +08:00
Jiayuan Zhang
c7e0863419 fix(auth): preserve last workspace ID across re-login (#772)
The logout handler was clearing `multica_workspace_id` from storage,
so re-login always defaulted to the first workspace. The workspace ID
is a user preference, not session-sensitive data — keep it so both
web and desktop restore the correct workspace after re-authentication.

Also pass `lastWorkspaceId` in the desktop login page, which was
previously missing.
2026-04-12 21:33:19 +08:00
Jiayuan Zhang
d7c83bc285 fix(sanitize): preserve code blocks and inline code from HTML entity escaping (#774)
Bluemonday operates on raw text, so characters like && and <> inside
markdown code blocks/inline code were being HTML-escaped (e.g. && → &amp;&amp;),
causing them to render incorrectly in the frontend.

Now extracts fenced code blocks and inline code spans before sanitization,
runs bluemonday on the remaining content, then restores the code verbatim.
2026-04-12 21:32:35 +08:00
Jiayuan Zhang
4285549381 fix(views): navigate to issue in same tab instead of opening new tab (#773)
Issue mention clicks now use push() for same-tab navigation, matching
AppLink behavior. Cmd/Ctrl+Click still opens in a new tab on desktop.
2026-04-12 17:29:54 +08:00
Bohan Jiang
9ed80120e0 fix(views): add missing useFileDropZone and FileDropOverlay mocks in create-issue test (#768)
The create-issue modal started importing useFileDropZone and FileDropOverlay
from the editor module, but the test mock was not updated to include them,
causing CI to fail.
2026-04-12 15:18:15 +08:00
Manish Chauhan
ec586ebc25 fix(pins): scope cache by user and fix sidebar pin action (#664) 2026-04-12 15:02:20 +08:00
Bohan Jiang
ea8cb18f9e Merge pull request #639 from jyf2100/agent/agent/e7cb5f8c
test(web): cover issue creation flow regressions
2026-04-12 14:17:47 +08:00
Bohan Jiang
d011039c58 fix(sweeper): add error logging and dedup for issue reset (#762)
- Log a warning when HasActiveTaskForIssue fails, matching the existing
  pattern for UpdateIssueStatus errors. Silent failures here make
  debugging DB issues unnecessarily difficult.
- Track processed issues to skip redundant GetIssue + HasActiveTaskForIssue
  queries when multiple tasks for the same issue are swept in one cycle.
2026-04-12 14:13:07 +08:00
Gabriel Amazonas
471d4a6838 Update required AI agent CLI list in SELF_HOSTING.md (#734)
Added OpenClaw and OpenCode to the list of required AI agent CLIs.
2026-04-12 14:09:57 +08:00
pradeep7127
bd42552854 fix(sweeper): reset in_progress issues to todo after stale task sweep (#747)
fix(sweeper): reset in_progress issues to todo after stale task sweep
2026-04-12 14:08:54 +08:00
Bohan Jiang
31eeb00b59 fix(storage): clean up variable shadowing and dead code (#761)
- Rename `filepath` local var to `dest` in LocalStorage.Upload to avoid
  shadowing the path/filepath package import
- Remove unused detectContentType and overrideContentType functions from
  util.go (no longer needed after ServeFile switched to http.ServeFile)
2026-04-12 14:06:46 +08:00
Antar Das
d32c419b6d feat(storage): add local file storage fallback (#710)
* feat(storage): add local file storage fallback

- Add local storage implementation for file uploads
- Update .env.example with LOCAL_UPLOAD_DIR and LOCAL_UPLOAD_BASE_URL
- Integrate local storage into server router and handlers
- Add storage abstraction layer with util functions

* ♻️ refactor(storage): improve path handling and file serving

switch from path to filepath for better cross-platform support and replace manual file serving logic with http.ServeFile to enhance security against path traversal. update unit tests to use t.Setenv for cleaner environment variable management.
2026-04-12 14:04:22 +08:00
Jiayuan Zhang
f31a322978 chore: add issue templates and improve PR template (#759)
* chore: add issue templates and improve PR template

Add GitHub issue templates (bug report, feature request) using YAML
forms, referencing hermes-agent's template structure. Update the PR
template with clearer sections for changes made, related issues, and
a more comprehensive checklist.

* chore: add AI disclosure section to PR template

Since most PRs are now authored or co-authored by AI coding tools,
add a dedicated AI Disclosure section to the PR template. Includes
authorship type, tool used, and a human review checklist to ensure
AI-generated code is properly reviewed before merge.

* chore: simplify AI disclosure to focus on prompt sharing

Remove the review-status checklist — it was too heavy and users won't
actually do it. Instead focus on what's useful: which AI tool was used
and what prompt/approach produced the code, so the team can learn from
each other's AI workflows.

* chore: simplify issue templates to lower submission friction

Bug report: just what happened + steps to reproduce (required),
plus an optional context field for logs/env.

Feature request: just what you want and why (required),
plus an optional proposed solution.

Removed all dropdowns, environment fields, checkboxes, and
other fields that discourage users from filing issues.

* chore: add screenshots section to issue templates

Add optional screenshots field to both bug report and feature request
templates so users can attach images for richer context.
2026-04-12 13:58:18 +08:00
Sanjay Ramadugu
f99f50eb0c feat(daemon): add Google Gemini CLI backend
Registers `gemini` as a sixth supported agent provider alongside claude,
codex, opencode, openclaw, and hermes.

- Daemon config probes for `gemini` on PATH (MULTICA_GEMINI_PATH /
  MULTICA_GEMINI_MODEL env overrides mirror the other providers).
- New agent.geminiBackend in pkg/agent/gemini.go: spawns
  `gemini -p <prompt> --yolo -o text [-m <model>] [-r <session>]`,
  reads stdout to completion, and returns a single MessageText plus
  the standard Result struct (Status / Output / DurationMs).
- Execution environment writes a GEMINI.md file into the task workdir
  (mirroring the existing CLAUDE.md / AGENTS.md injection for other
  providers) so Gemini discovers the Multica runtime meta-skill
  through its native mechanism.

Tests:

- pkg/agent/gemini_test.go — unit coverage for buildGeminiArgs
  (baseline, model override, resume session, omit-when-empty).
- internal/daemon/execenv/TestInjectRuntimeConfigGemini — verifies
  GEMINI.md is written and that CLAUDE.md/AGENTS.md are NOT.

Scope (intentional for v1):

- Text output only (`-o text`). Streaming tool events via
  `--output-format stream-json` is a follow-up once we have a
  reliable reproduction of Gemini's event schema.
- No MCP config plumbing. Gemini's `--allowed-mcp-server-names`
  filter pairs well with the per-agent MCP work on feat/per-agent-mcp;
  stacking the two can land as a follow-up.
- No token usage scraping (Gemini's accounting lives on the Google
  Cloud side, not a local JSONL log like claude/codex).
- No session resume wiring beyond accepting the ExecOptions field —
  the daemon does not yet persist Gemini session IDs because the text
  output mode does not expose them.

Migration / env changes:

- New optional environment variables MULTICA_GEMINI_PATH and
  MULTICA_GEMINI_MODEL. Default path is the string "gemini" (resolved
  via PATH at daemon startup). If no Gemini install is detected, the
  provider is simply absent from the runtime — no behavior change for
  existing deployments.
2026-04-11 22:58:49 -04:00
Jiayuan Zhang
5bae3368d7 feat(landing): add install command copy block to hero section (#743)
Adds a terminal-style one-click copy block below the CTA buttons showing
the curl install command, with a copy-to-clipboard button that shows a
checkmark on success.
2026-04-12 02:42:05 +08:00
Jiayuan Zhang
f100b5b707 fix(auth): graceful email degradation for self-hosting (#742)
* fix(auth): log email send errors and gracefully degrade in non-production

In non-production environments (APP_ENV != "production"), if sending the
verification code email fails, log the error as a warning and still return
success. This lets self-hosting users log in with the master code (888888)
even when their Resend configuration is incomplete (e.g. unverified from-domain).

In production, the behavior is unchanged — email failures return 500.

Also adds guidance in .env.example about RESEND_FROM_EMAIL for self-hosters.

Closes #723

* fix(auth): remove APP_ENV degradation, keep error logging only

Remove the APP_ENV-based graceful degradation for email send failures
— it's risky if users forget to set APP_ENV=production. Instead, always
return 500 on email failure (safe for production) and rely on the error
log (slog.Error) with the actual Resend error for debugging.

Self-hosters who don't need real emails should leave RESEND_API_KEY empty
(codes print to stdout, master code 888888 works).
2026-04-12 02:30:01 +08:00
Jiayuan Zhang
701399536f feat(cli): enhance version command with JSON output and build info (#740)
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:18:08 +08:00
Jiayuan Zhang
4ca607f888 chore: remove Apache 2.0 license badge from READMEs (#739) 2026-04-12 02:11:45 +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
roc
a35f71f65d test(web): cover issue creation flow regressions 2026-04-10 15:52:43 +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
807 changed files with 81954 additions and 12906 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

@@ -22,6 +22,8 @@ MULTICA_CODEX_WORKDIR=
MULTICA_CODEX_TIMEOUT=20m
# Email (Resend)
# For local/dev use, leave RESEND_API_KEY empty — codes print to stdout, and master code 888888 works.
# For production, set your Resend API key and change RESEND_FROM_EMAIL to a domain verified in your Resend account.
RESEND_API_KEY=
RESEND_FROM_EMAIL=noreply@multica.ai
@@ -29,6 +31,7 @@ RESEND_FROM_EMAIL=noreply@multica.ai
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
NEXT_PUBLIC_GOOGLE_CLIENT_ID=
# S3 / CloudFront
S3_BUCKET=
@@ -39,11 +42,23 @@ CLOUDFRONT_PRIVATE_KEY=
CLOUDFRONT_DOMAIN=
COOKIE_DOMAIN=
# Local file storage (fallback when S3_BUCKET is not set)
LOCAL_UPLOAD_DIR=./data/uploads
LOCAL_UPLOAD_BASE_URL=http://localhost:8080
# Security
# Comma-separated list of allowed origins for CORS and WebSocket connections.
# Defaults to localhost dev origins when unset.
# Example: ALLOWED_ORIGINS=https://app.multica.ai,https://staging.multica.ai
ALLOWED_ORIGINS=
# Frontend
FRONTEND_PORT=3000
FRONTEND_ORIGIN=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws
# Leave empty — auto-derived from page origin in browser, set by Makefile for local dev.
# Only set explicitly if frontend and backend are on different domains.
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_WS_URL=
# Remote API (optional) — set to proxy local frontend to a remote backend
# Leave empty to use local backend (localhost:8080)

File diff suppressed because one or more lines are too long

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

39
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: "Bug Report"
description: Report a bug — something that's broken, crashes, or behaves incorrectly.
title: "[Bug]: "
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: What happened?
description: Describe the bug and what you expected instead. Screenshots, error messages, or screen recordings are welcome.
placeholder: |
When I do X, Y happens. I expected Z instead.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: How can we trigger this bug?
placeholder: |
1. Go to '...'
2. Click on '...'
3. See error
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots (optional)
description: If applicable, add screenshots or screen recordings to help explain the problem.
- type: textarea
id: context
attributes:
label: Additional context (optional)
description: Environment info, logs, or anything else that might help.
render: shell

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: true

View File

@@ -0,0 +1,26 @@
name: "Feature Request"
description: Suggest a new feature or improvement.
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: textarea
id: description
attributes:
label: What do you want and why?
description: Describe the problem you're trying to solve or the improvement you'd like to see.
placeholder: |
I'm trying to do X but there's no way to...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution (optional)
description: If you have an idea for how this should work, describe it here.
- type: textarea
id: screenshots
attributes:
label: Screenshots / mockups (optional)
description: If applicable, add screenshots, mockups, or sketches to illustrate your idea.

58
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,58 @@
## What does this PR do?
<!-- Describe the change clearly. What problem does it solve? Why is this approach the right one? -->
## Related Issue
<!-- Link the issue this PR addresses. If no issue exists, consider creating one first. -->
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Refactor / code improvement (no behavior change)
- [ ] Documentation update
- [ ] Tests (adding or improving test coverage)
- [ ] CI / infrastructure
## Changes Made
<!-- List the specific changes. Include file paths for code changes. -->
-
## How to Test
<!-- Steps to verify this change works. For bugs: reproduction steps + proof that the fix works. -->
1.
2.
3.
## Checklist
- [ ] I have included a thinking path that traces from project context to this change
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [ ] I have considered and documented any risks above
- [ ] I will address all reviewer comments before requesting merge
## AI Disclosure
<!-- Most PRs involve AI coding tools — that's totally fine! We're curious about your process. -->
**AI tool used:** <!-- e.g. Claude Code, Cursor, GitHub Copilot, Multica Agent, N/A -->
**Prompt / approach:**
<!-- How did you use AI to produce this code? Share your prompt, conversation link, or describe your approach. This helps the team learn from each other's AI workflows. -->
## Screenshots (optional)
<!-- If applicable, add screenshots showing the change in action. -->

View File

@@ -57,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

13
.gitignore vendored
View File

@@ -12,10 +12,17 @@ build
bin
dist-electron
*.tsbuildinfo
# ...except electron-builder's source resources dir, which holds tracked
# config files (entitlements, icons) — not build output.
!apps/desktop/build/
!apps/desktop/build/**
# env
.env*
!.env.example
# Desktop production config is public (backend URL, etc.) — track it so
# `pnpm package` produces a release-ready build without extra setup.
!apps/desktop/.env.production
# test coverage
coverage
@@ -36,11 +43,17 @@ apps/web/test-results/
# local settings
.claude/
.tool-versions
# feature tracking
_features/
# runtime
*.pid
# platform specific
*.dmg
*.app
server/server
data/
.kilo

View File

@@ -11,19 +11,28 @@ builds:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.ShortCommit}}
- -X main.date={{.Date}}
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
archives:
- id: default
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
checksum:
@@ -47,7 +56,7 @@ brews:
directory: Formula
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: |

View File

@@ -1,16 +1,47 @@
# Repository Guidelines
## Project Structure & Module Organization
`apps/web/` contains the Next.js 16 frontend: routes live in `app/`, reusable UI in `components/`, feature code in `features/`, test utilities in `test/`, and static assets in `public/`. `server/` contains the Go backend: entry points are in `cmd/{server,multica,migrate}`, application logic lives in `internal/`, migrations are in `migrations/`, and SQL lives under `pkg/db/queries/` with generated sqlc output in `pkg/db/generated/`. `e2e/` holds Playwright coverage. `scripts/` and the root `Makefile` drive local setup and verification.
This file provides guidance to AI agents when working with code in this repository.
## Build, Test, and Development Commands
Use `make setup` for first-time setup: it installs dependencies, ensures PostgreSQL is running, and applies migrations. Use `make start` to run backend and frontend together with `.env` or `.env.worktree`. For single-surface work, use `pnpm dev:web` for the frontend and `make dev` for the Go server. Run `pnpm test` for Vitest, `make test` for Go tests, and `make check` for the full pipeline: typecheck, frontend unit tests, Go tests, then Playwright. After changing SQL in `server/pkg/db/queries/*.sql`, run `make sqlc`.
> **Single source of truth:** This file is a concise pointer document.
> All authoritative architecture, coding rules, commands, and conventions
> live in **CLAUDE.md** at the project root. Read that file first.
## Coding Style & Naming Conventions
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 should stay `gofmt`-clean and use domain-oriented filenames like `issue.go` or `cmd_issue.go`. Do not hand-edit generated code in `server/pkg/db/generated/`.
## Quick Reference
## Testing Guidelines
Frontend unit tests use Vitest with Testing Library and shared setup from `apps/web/test/`. 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. Backend tests use Gos standard `*_test.go` pattern. Add or update tests whenever you change handlers, CLI commands, daemon behavior, or SQL-backed flows.
### Architecture
## Commit & Pull Request Guidelines
Recent history follows conventional commits with scopes, for example `feat(web): ...`, `fix(cli): ...`, `refactor(daemon): ...`, `test(cli): ...`, and `docs: ...`. 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.
Go backend + monorepo frontend (pnpm workspaces + Turborepo) with shared packages.
- `server/` — Go backend (Chi router, sqlc, gorilla/websocket)
- `apps/web/` — Next.js frontend (App Router)
- `apps/desktop/` — Electron desktop app
- `packages/core/` — Headless business logic (Zustand stores, React Query hooks, API client)
- `packages/ui/` — Atomic UI components (shadcn/Base UI, zero business logic)
- `packages/views/` — Shared business pages/components
- `packages/tsconfig/` — Shared TypeScript config
### State Management (critical)
- **React Query** owns all server state (issues, members, agents, inbox, workspace list)
- **Zustand** owns all client state (current workspace selection, view filters, drafts, modals)
- All Zustand stores live in `packages/core/` — never in `packages/views/` or app directories
- WS events invalidate React Query — never write directly to stores
### Package Boundaries (hard rules)
- `packages/core/` — zero react-dom, zero localStorage, zero process.env
- `packages/ui/` — zero `@multica/core` imports
- `packages/views/` — zero `next/*`, zero `react-router-dom`, use `NavigationAdapter` for routing
- `apps/web/platform/` — only place for Next.js APIs
### Commands
```bash
make dev # Auto-setup + start everything
pnpm typecheck # TypeScript check
pnpm test # TS unit tests (Vitest)
make test # Go tests
make check # Full verification pipeline
```
See CLAUDE.md for the complete command reference.

307
CLAUDE.md
View File

@@ -12,119 +12,71 @@ Multica is an AI-native task management platform — like Linear, but with AI ag
## Architecture
**Go backend + standalone Next.js 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) — self-contained, no shared package dependencies
- `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 four 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, types, logger)
```
**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` |
| `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.
**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 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";
```
- **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 (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).
- 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 frontend
pnpm typecheck # TypeScript check
pnpm lint # ESLint via Next.js
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")
@@ -133,15 +85,24 @@ make sqlc # Regenerate sqlc code after editing SQL in server/pkg/db/
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 (pgvector/pg17 image)
make db-down # Stop shared PostgreSQL
@@ -149,12 +110,14 @@ 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`.
CI runs on Node 22 and Go 1.26.1 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`.
`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
@@ -169,33 +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 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.
- 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
@@ -208,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)
```
@@ -221,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).

397
CLI_AND_DAEMON.md Normal file
View File

@@ -0,0 +1,397 @@
# 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 install multica-ai/tap/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
brew upgrade multica-ai/tap/multica
```
For install script or manual installs, use:
```bash
multica update
```
`multica update` auto-detects your installation method and upgrades accordingly.
## Quick Start
```bash
# One-command setup: configure, authenticate, and start the daemon
multica setup
# For self-hosted (local) deployments:
multica setup self-host
```
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 |
| OpenCode | `opencode` | Open-source coding agent |
| OpenClaw | `openclaw` | Open-source coding agent |
| Hermes | `hermes` | Nous Research 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 |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
| `MULTICA_OPENCLAW_MODEL` | Override the OpenClaw model used |
| `MULTICA_HERMES_PATH` | Custom path to the `hermes` binary |
| `MULTICA_HERMES_MODEL` | Override the Hermes model used |
### Self-Hosted Server
When connecting to a self-hosted Multica instance, the easiest approach is:
```bash
# One command — configures for localhost, authenticates, starts daemon
multica setup self-host
# Or for on-premise with custom domains:
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
```
Or configure manually:
```bash
# Set URLs individually
multica config set server_url http://localhost:8080
multica config set app_url http://localhost:3000
# For production with TLS:
# multica config set server_url https://api.example.com
# multica config set app_url https://app.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
# Set up a staging profile
multica setup self-host --profile staging --server-url https://api-staging.example.com --app-url https://staging.example.com
# Start its daemon
multica daemon start --profile staging
# 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 for Multica Cloud: configure, authenticate, and start the daemon
multica setup
# For local self-hosted deployments
multica setup self-host
# Custom ports
multica setup self-host --port 9090 --frontend-port 4000
# On-premise with custom domains
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
```
`multica setup` configures the CLI, opens your browser for authentication, and starts the daemon — all in one step. Use `multica setup self-host` to connect to a self-hosted server instead of Multica Cloud.
## 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 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
```

200
CLI_INSTALL.md Normal file
View File

@@ -0,0 +1,200 @@
# 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
> **Windows users:** Skip to [Option C: Windows (PowerShell)](#option-c-windows-powershell) below.
### Option A: Homebrew (preferred — macOS/Linux)
Check if Homebrew is available:
```bash
which brew
```
If `brew` is found, install via Homebrew:
```bash
brew install multica-ai/tap/multica
```
Then verify:
```bash
multica version
```
If the version prints successfully, skip to **Step 3**.
To upgrade later, run:
```bash
brew upgrade multica-ai/tap/multica
```
### Option B: Download from GitHub Releases (macOS/Linux, 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`.
### Option C: Windows (PowerShell)
Run in PowerShell (no admin required):
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
This downloads the latest Windows binary from GitHub Releases, installs it to `%USERPROFILE%\.multica\bin\`, and adds it to your user PATH.
Verify:
```powershell
multica version
```
**If this fails:**
- Restart your terminal so the updated PATH takes effect.
- If you use Scoop, the installer will use it automatically: `scoop bucket add multica https://github.com/multica-ai/scoop-bucket.git && scoop install multica`
- If your execution policy blocks the script: `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned` then re-run.
---
## 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`, `opencode`, `openclaw`, `hermes`).
**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`, `codex`, `opencode`, `openclaw`, or `hermes`) 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`, `opencode`, `openclaw`, or `hermes`)
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 supported CLI (`claude`, `codex`, `opencode`, `openclaw`, or `hermes`), 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:
@@ -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
@@ -171,17 +164,15 @@ Use a worktree when you want isolated data and separate app ports.
```bash
git worktree add ../multica-feature -b feat/my-change main
cd ../multica-feature
make worktree-env
make setup-worktree
make start-worktree
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
@@ -424,9 +415,7 @@ Warning:
### Stable Main Environment
```bash
cp .env.example .env
make setup-main
make start-main
make dev
```
### Feature Worktree
@@ -434,9 +423,7 @@ make start-main
```bash
git worktree add ../multica-feature -b feat/my-change main
cd ../multica-feature
make worktree-env
make setup-worktree
make start-worktree
make dev
```
### Return to a Previously Configured Worktree

View File

@@ -30,7 +30,9 @@ 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 ["./server"]
ENTRYPOINT ["./entrypoint.sh"]

72
Dockerfile.web Normal file
View File

@@ -0,0 +1,72 @@
# --- 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
ARG NEXT_PUBLIC_WS_URL
ENV REMOTE_API_URL=$REMOTE_API_URL
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
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 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
.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
@@ -36,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 self-host"; \
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
@@ -69,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:
@@ -98,8 +150,12 @@ check-main:
@ENV_FILE=$(MAIN_ENV_FILE) bash scripts/check.sh
setup-worktree:
@echo "==> Generating $(WORKTREE_ENV_FILE) with unique ports..."
@FORCE=1 bash scripts/init-worktree-env.sh $(WORKTREE_ENV_FILE)
@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:
@@ -113,8 +169,12 @@ 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
@@ -130,14 +190,17 @@ multica:
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

335
README.md
View File

@@ -1,211 +1,196 @@
<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)
[![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/MulticaAI) · [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
### macOS / Linux (Homebrew - recommended)
```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
brew install multica-ai/tap/multica
```
Open your configured `FRONTEND_ORIGIN` in the browser. By default that is [http://localhost:3000](http://localhost:3000).
Use `brew upgrade multica-ai/tap/multica` to keep the CLI current.
Main checkout uses `.env`. A Git worktree should generate its own `.env.worktree` and use the explicit worktree targets:
### macOS / Linux (install script)
```bash
make worktree-env
make setup-worktree
make start-worktree
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
Every checkout shares the same PostgreSQL container on `localhost:5432`. Isolation now happens at the database level:
Use this if Homebrew is not available. The script installs the Multica CLI on macOS and Linux by using Homebrew when it is on `PATH`, otherwise it downloads the binary directly.
- `.env` typically uses `POSTGRES_DB=multica`
- each `.env.worktree` gets its own `POSTGRES_DB`, such as `multica_my_feature_702`
- backend/frontend ports still stay unique per worktree
### Windows (PowerShell)
That keeps one Docker container and one volume, while still isolating schema and data per worktree.
## Project Structure
```
├── 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
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
## Commands
Then configure, authenticate, and start the daemon in one command:
### Frontend
```bash
multica setup # Connect to Multica Cloud, log in, start daemon
```
> **Self-hosting?** Add `--with-server` 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 -- --with-server
> multica setup self-host
> ```
>
> Requires Docker. See the [Self-Hosting Guide](SELF_HOSTING.md) for details.
---
## Getting Started
### 1. Set up and start the daemon
```bash
multica setup # Configure, authenticate, and start the daemon
```
The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) 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.
---
## Multica vs Paperclip
| | Multica | Paperclip |
|---|---------|-----------|
| **Focus** | Team AI agent collaboration platform | Solo AI agent company simulator |
| **User model** | Multi-user teams with roles & permissions | Single board operator |
| **Agent interaction** | Issues + Chat conversations | Issues + Heartbeat |
| **Deployment** | Cloud-first | Local-first |
| **Management depth** | Lightweight (Issues / Projects / Labels) | Heavy governance (Org chart / Approvals / Budgets) |
| **Extensibility** | Skills system | Skills + Plugin system |
**TL;DR — Multica is built for teams that want to collaborate with AI agents on real projects together.**
---
## 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 for Multica Cloud (configure + login + start daemon) |
| `multica setup self-host` | Same, but for self-hosted deployments |
| `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 multica ARGS="version"` | Run the local `multica` CLI without installing it |
| `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` |
## CLI (`multica`)
The CLI manages authentication, workspace configuration, and the local agent daemon.
### Install
```bash
brew tap multica-ai/tap
brew install multica-cli
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go Backend │────>│ PostgreSQL │
│ Frontend │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (runs on your machine)
│Claude/Codex/ │
│OpenClaw/Code │
└──────────────┘
```
Or build from source:
| 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 |
## Development
For contributors working on the Multica codebase, see the [Contributing Guide](CONTRIBUTING.md).
**Prerequisites:** [Node.js](https://nodejs.org/) v20+, [pnpm](https://pnpm.io/) v10.28+, [Go](https://go.dev/) v1.26+, [Docker](https://www.docker.com/)
```bash
make build
cp server/bin/multica /usr/local/bin/multica # or ~/.local/bin/multica
make dev
```
For local development, you can also run the CLI directly from the repo:
`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.
```bash
make multica ARGS="version"
make multica ARGS="auth status"
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow, worktree support, testing, and troubleshooting.
For browser-based auth from source, make sure the local frontend is running at `FRONTEND_ORIGIN` first, for example with `make start`, `make start-main`, or `make start-worktree`.
## Star History
### Authentication
```bash
multica login # Authenticate and auto-watch your workspaces
multica auth login # Legacy auth-only flow
multica auth login --token # Legacy token-only auth flow
multica auth status # Show current auth status
multica auth logout # Remove stored token
```
Credentials are saved to `~/.multica/config.json`.
### Workspaces
```bash
multica workspace list # List all workspaces you belong to
multica workspace get # Show the current workspace details/context
```
### Daemon Watch List
The daemon monitors one or more workspaces for tasks. Manage which workspaces are watched:
```bash
multica workspace watch <workspace-id> # Add a workspace to the watch list
multica workspace unwatch <workspace-id> # Remove a workspace from the watch list
multica workspace list # Show all workspaces (watched ones marked with *)
```
The watch list is stored in `~/.multica/config.json`. Changes are picked up by a running daemon within 5 seconds (hot-reload).
### Local Agent Daemon
The daemon polls watched workspaces for tasks and executes them using locally installed AI agents (Claude Code, Codex).
```bash
# 1. Authenticate
multica login
# 2. Add workspaces to watch
multica workspace watch <workspace-id>
# 3. Start the daemon
multica daemon start
```
The daemon auto-detects available agent CLIs (`claude`, `codex`) on your PATH. When a task is claimed, it creates an isolated execution environment, runs the agent, and reports results back to the server.
### Other Commands
```bash
multica agent list # List agents in the current workspace
multica daemon status # Show local daemon status
multica config # Show CLI configuration
multica config show # Compatibility alias for config display
multica version # Show CLI version
```
## Environment Variables
See [`.env.example`](.env.example) for all available variables:
- `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 for CLI login callback (default: `http://localhost:3000`)
- `MULTICA_DAEMON_ID` / `MULTICA_DAEMON_DEVICE_NAME` — Stable daemon identity for runtime registration
- `MULTICA_CLAUDE_PATH` / `MULTICA_CLAUDE_MODEL` — Claude Code executable and optional model override
- `MULTICA_CODEX_PATH` / `MULTICA_CODEX_MODEL` — Codex executable and optional model override
- `MULTICA_WORKSPACES_ROOT` — Base directory for agent execution environments (default: `~/multica_workspaces`)
- `NEXT_PUBLIC_API_URL` — Frontend → backend API URL
- `NEXT_PUBLIC_WS_URL` — Frontend → backend WebSocket URL
## 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.
<a href="https://www.star-history.com/?repos=multica-ai%2Fmultica&type=date&legend=bottom-right">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
</picture>
</a>

184
README.zh-CN.md Normal file
View File

@@ -0,0 +1,184 @@
<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)
[![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/MulticaAI) · [自部署指南](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 和设置。
---
## 快速安装
### macOS / Linux推荐 Homebrew
```bash
brew install multica-ai/tap/multica
```
后续可用 `brew upgrade multica-ai/tap/multica` 更新 CLI。
### macOS / Linux安装脚本
```bash
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
如果没有 Homebrew可以使用安装脚本。脚本会安装 Multica CLI检测到 `brew` 时通过 Homebrew 安装,否则直接下载二进制。
### Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
安装完成后,一条命令完成配置、认证和启动:
```bash
multica setup # 连接 Multica Cloud登录启动 daemon
```
> **自部署?** 加上 `--with-server` 在本地部署完整的 Multica 服务:
>
> ```bash
> curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
> multica setup self-host
> ```
>
> 需要 Docker。详见 [自部署指南](SELF_HOSTING.md)。
---
## 快速上手
安装好 CLI或注册 [Multica 云服务](https://multica.ai))后,按以下步骤将第一个任务分配给 Agent
### 1. 配置并启动 daemon
```bash
multica setup # 配置、认证、启动 daemon一条命令搞定
```
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 现在是团队的一员了。 🎉
---
## Multica vs Paperclip
| | Multica | Paperclip |
|---|---------|-----------|
| **定位** | 团队 AI Agent 协作平台 | 个人 AI Agent 公司模拟器 |
| **用户模型** | 多人团队,角色权限 | 单人 Board Operator |
| **Agent 交互** | Issue + Chat 对话 | Issue + Heartbeat |
| **部署** | 云端优先 | 本地优先 |
| **管理深度** | 轻量Issue / Project / Labels | 重度(组织架构 / 审批 / 预算) |
| **扩展** | Skills 系统 | Skills + 插件系统 |
**简单来说Multica 专为团队协作打造,让团队和 AI 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)
## Star History
<a href="https://www.star-history.com/?repos=multica-ai%2Fmultica&type=date&legend=bottom-right">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=multica-ai/multica&type=date&legend=top-left" />
</picture>
</a>

212
SELF_HOSTING.md Normal file
View File

@@ -0,0 +1,212 @@
# 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)
Two commands to set up everything — server, CLI, and configuration:
```bash
# 1. Install CLI + provision the self-host server
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
# 2. Configure CLI, authenticate, and start the daemon
multica setup self-host
```
This clones the repository, starts all services via Docker Compose, installs the `multica` CLI, then configures it for localhost.
Open http://localhost:3000, log in with any email + verification code **`888888`**.
> **Prerequisites:** Docker and Docker Compose must be installed. The script checks for this and provides install links if missing.
>
> **CLI only?** If the self-host server is already running and you only need the CLI on a macOS/Linux machine, install it with Homebrew:
>
> ```bash
> brew install multica-ai/tap/multica
> ```
---
## 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)
- [OpenClaw](https://github.com/openclaw/openclaw) (`openclaw` on PATH)
- [OpenCode](https://github.com/anomalyco/opencode) (`opencode` on PATH)
- [Hermes](https://github.com/NousResearch/hermes) (`hermes` on PATH)
### b) One-command setup
```bash
multica setup self-host
```
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
For on-premise deployments with custom domains:
```bash
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
```
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 setup
```
This reconfigures the CLI for multica.ai, re-authenticates, and restarts the daemon. You will be prompted before overwriting the existing configuration.
> 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 set server_url http://localhost:8080
multica config set app_url http://localhost:3000
# 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).

259
SELF_HOSTING_ADVANCED.md Normal file
View File

@@ -0,0 +1,259 @@
# 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 |
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 |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
| `MULTICA_OPENCLAW_MODEL` | Override the OpenClaw model used |
| `MULTICA_HERMES_PATH` | Custom path to the `hermes` binary |
| `MULTICA_HERMES_MODEL` | Override the Hermes model used |
## 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
```
## LAN / Non-localhost Access
By default, Multica works on `localhost`. If you access it from another machine on the LAN (e.g. `http://192.168.1.100:3000`), you need to tell the backend to accept that origin:
```bash
# .env — replace with your server's LAN IP
FRONTEND_ORIGIN=http://192.168.1.100:3000
CORS_ALLOWED_ORIGINS=http://192.168.1.100:3000
```
Then rebuild:
```bash
docker compose -f docker-compose.selfhost.yml up -d --build
```
The frontend automatically derives the WebSocket URL from the page address, so real-time features (chat streaming, live issue updates, notifications) work over LAN without extra configuration.
> **Note:** If you need to override the WebSocket URL explicitly (e.g. when using a separate backend domain), set `NEXT_PUBLIC_WS_URL` in `.env` and rebuild the frontend image.
## 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.

76
SELF_HOSTING_AI.md Normal file
View File

@@ -0,0 +1,76 @@
# 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`
## Install
```bash
# Install CLI + provision self-host server
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
# Configure CLI for localhost, authenticate, and start daemon
multica setup self-host
```
Wait for the server output `✓ Multica server is running and CLI is ready!` before running `multica setup self-host`.
**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 self-host
```
The `multica setup self-host` 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 self-host --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`

View File

@@ -0,0 +1,12 @@
# Production environment for `pnpm package` / `pnpm build`.
# electron-vite (Vite under the hood) reads this automatically in
# production mode and inlines the values into the renderer bundle via
# import.meta.env.VITE_*. These are public URLs, not secrets.
# Backend API + websocket the desktop app talks to.
VITE_API_URL=https://api.multica.ai
VITE_WS_URL=wss://api.multica.ai/ws
# Public web app URL — used to build shareable links like "Copy link to
# issue" that users paste into Slack / messages. See platform/navigation.tsx.
VITE_APP_URL=https://multica.ai

8
apps/desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
dist
out
.DS_Store
.eslintcache
*.log*
# CLI binary bundled at build time (from server/bin/)
resources/bin/

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Electron / V8 need JIT and unsigned executable memory under the
hardened runtime. -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Required so the app can spawn the bundled `multica` Go binary and
any other child processes (e.g. agent CLIs) without Gatekeeper
blocking exec. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- Network client — the daemon talks to the backend + GitHub releases. -->
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,45 @@
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}"
protocols:
- name: Multica
schemes:
- multica
asarUnpack:
- resources/**
mac:
entitlementsInherit: build/entitlements.mac.plist
target:
- dmg
- zip
# Hardcoded name avoids the `@multica/desktop-*` subdirectory that
# `${name}` produces for scoped package names.
artifactName: multica-desktop-${version}-${arch}.${ext}
# Notarize via notarytool. Requires APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD
# + APPLE_TEAM_ID env vars at package time. Non-mac contributors are
# unaffected because `pnpm package` already requires the Developer ID
# signing cert — notarization is a strict superset.
notarize: true
dmg:
artifactName: multica-desktop-${version}-${arch}.${ext}
linux:
target:
- AppImage
- deb
artifactName: ${name}-${version}-${arch}.${ext}
win:
target:
- nsis
artifactName: ${name}-${version}-setup.${ext}
publish:
provider: github
owner: multica-ai
repo: multica
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,13 @@
import globals from "globals";
import reactConfig from "@multica/eslint-config/react";
export default [
...reactConfig,
{ ignores: ["out/", "dist/"] },
{
files: ["scripts/**/*.{mjs,js}"],
languageOptions: {
globals: { ...globals.node },
},
},
];

54
apps/desktop/package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "@multica/desktop",
"version": "0.1.0",
"private": true,
"main": "./out/main/index.js",
"scripts": {
"bundle-cli": "node scripts/bundle-cli.mjs",
"dev": "pnpm run bundle-cli && electron-vite dev",
"build": "pnpm run bundle-cli && 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": "node scripts/package.mjs",
"lint": "eslint .",
"test": "vitest run",
"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",
"@fontsource-variable/inter": "^5.2.5",
"@fontsource/geist-mono": "^5.2.7",
"@multica/core": "workspace:*",
"@multica/ui": "workspace:*",
"@multica/views": "workspace:*",
"electron-updater": "^6.8.3",
"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:",
"vitest": "catalog:"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env node
// Builds the `multica` CLI from server/cmd/multica and copies the binary
// into apps/desktop/resources/bin/ so electron-vite (dev) and electron-
// builder (prod) pick it up. Running this on every dev/build/package
// invocation guarantees the bundled CLI always matches the current Go
// source — no more stale binary surprises. Go's build cache makes the
// no-op case (nothing changed) effectively free.
//
// ldflags mirror `make build` so `multica --version` reports a meaningful
// version / commit / date.
//
// Graceful: if `go` is not installed (e.g. frontend-only contributor), we
// skip the build and fall through to auto-install at runtime. A genuine
// Go compile error is fatal — you want that to block dev, not hide.
import { access, chmod, copyFile, mkdir } from "node:fs/promises";
import { constants } from "node:fs";
import { execFileSync, execSync } from "node:child_process";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, "..", "..", "..");
const serverDir = join(repoRoot, "server");
const binName = process.platform === "win32" ? "multica.exe" : "multica";
const srcBinary = join(serverDir, "bin", binName);
const destDir = join(repoRoot, "apps", "desktop", "resources", "bin");
const destBinary = join(destDir, binName);
function sh(cmd) {
try {
return execSync(cmd, { encoding: "utf-8" }).trim();
} catch {
return "";
}
}
function hasGo() {
try {
execSync("go version", { stdio: "pipe" });
return true;
} catch {
return false;
}
}
async function exists(p) {
try {
await access(p, constants.F_OK);
return true;
} catch {
return false;
}
}
if (hasGo()) {
const version = sh("git describe --tags --always --dirty") || "dev";
const commit = sh("git rev-parse --short HEAD") || "unknown";
const date = new Date().toISOString().replace(/\.\d+Z$/, "Z");
const ldflags = `-X main.version=${version} -X main.commit=${commit} -X main.date=${date}`;
console.log(
`[bundle-cli] go build → ${srcBinary} (version=${version} commit=${commit})`,
);
execFileSync(
"go",
[
"build",
"-ldflags",
ldflags,
"-o",
join("bin", binName),
"./cmd/multica",
],
{ cwd: serverDir, stdio: "inherit" },
);
} else {
console.warn(
"[bundle-cli] `go` not found in PATH — skipping CLI build. " +
"Desktop will use whatever is already in resources/bin/, or fall back " +
"to auto-installing the latest release at runtime.",
);
}
if (!(await exists(srcBinary))) {
console.warn(
`[bundle-cli] ${srcBinary} not present — Desktop will fall back to ` +
`auto-installing the latest release at runtime.`,
);
process.exit(0);
}
await mkdir(destDir, { recursive: true });
await copyFile(srcBinary, destBinary);
await chmod(destBinary, 0o755);
// macOS: ad-hoc sign so Gatekeeper doesn't complain when the parent app
// (which itself may be unsigned in dev) spawns the child.
if (process.platform === "darwin") {
try {
execSync(`codesign -s - --force ${JSON.stringify(destBinary)}`, {
stdio: "pipe",
});
} catch {
// Non-fatal. Unsigned binaries still run when the parent app is trusted.
}
}
console.log(`[bundle-cli] bundled ${srcBinary}${destBinary}`);

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env node
// Wrapper around `electron-builder` that keeps the Desktop version in
// lockstep with the CLI. Both are derived from `git describe --tags
// --always --dirty` — the same source GoReleaser reads for the CLI
// binary via the `main.version` ldflag — so a single `vX.Y.Z` tag push
// produces matching CLI and Desktop versions.
//
// Runs the existing bundle-cli.mjs first (so the Go binary is compiled
// and copied into resources/bin/), then invokes electron-builder with
// `-c.extraMetadata.version=<derived>` so the override applies at build
// time without mutating the tracked package.json.
//
// Extra CLI args after `pnpm package --` are forwarded to electron-builder
// unchanged (e.g. `--mac --arm64`).
//
// The `normalizeGitVersion` helper is exported so tests can cover the
// version-derivation logic without shelling out.
import { execFileSync, spawnSync, execSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(here, "..");
function sh(cmd) {
try {
return execSync(cmd, { encoding: "utf-8" }).trim();
} catch {
return "";
}
}
/**
* Pure transformation from the `git describe --tags --always --dirty`
* output to the value we feed into electron-builder's extraMetadata.version.
*
* - empty input → null (caller should fall back)
* - "v0.1.36" → "0.1.36"
* - "v0.1.35-14-gf1415e96" → "0.1.35-14-gf1415e96" (semver prerelease)
* - "v0.1.35-…-dirty" → same, dirty suffix preserved
* - "f1415e96" (no tag) → "0.0.0-f1415e96" (fallback)
*
* Leading `v` is stripped so the result is valid semver for package.json.
*/
export function normalizeGitVersion(raw) {
if (!raw) return null;
const stripped = raw.replace(/^v/, "");
if (!/^\d/.test(stripped)) {
// No reachable tag — `git describe` fell back to just the commit hash.
return `0.0.0-${stripped}`;
}
return stripped;
}
function deriveVersion() {
return normalizeGitVersion(sh("git describe --tags --always --dirty"));
}
function main() {
// Step 1: build + bundle the Go CLI via the existing script.
execFileSync("node", [resolve(here, "bundle-cli.mjs")], {
stdio: "inherit",
cwd: desktopRoot,
});
// Step 2: derive the version that should be written into the app.
const version = deriveVersion();
if (version) {
console.log(`[package] Desktop version → ${version} (from git describe)`);
} else {
console.warn(
"[package] could not derive version from git; falling back to package.json",
);
}
// Step 3: assemble electron-builder args.
const passthrough = process.argv.slice(2);
const builderArgs = [];
if (version) builderArgs.push(`-c.extraMetadata.version=${version}`);
// Step 4: gracefully degrade for local dev builds. electron-builder.yml
// sets `notarize: true` so real releases notarize in-build (keeping the
// stapled .app consistent with latest-mac.yml's SHA512). But a mac dev
// who just wants to smoke-test a local package doesn't have Apple
// credentials, and would otherwise hit a hard failure at the notarize
// step. Detect the missing env and flip notarize off for this run only.
if (!process.env.APPLE_TEAM_ID) {
console.warn(
"[package] APPLE_TEAM_ID not set — skipping notarization (local dev build). " +
"Set APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID for a release build.",
);
builderArgs.push("-c.mac.notarize=false");
}
builderArgs.push(...passthrough);
// Step 5: invoke electron-builder. pnpm puts node_modules/.bin on PATH
// for the script run, so spawnSync finds the binary without needing a
// shell wrapper (avoids any risk of argv interpolation).
const result = spawnSync("electron-builder", builderArgs, {
stdio: "inherit",
cwd: desktopRoot,
});
if (result.error) {
console.error(
"[package] failed to spawn electron-builder:",
result.error.message,
);
process.exit(1);
}
process.exit(result.status ?? 1);
}
// Only run when invoked as a CLI, not when imported by a test file.
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main();
}

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { normalizeGitVersion } from "./package.mjs";
describe("normalizeGitVersion", () => {
it("returns null for empty / nullish input", () => {
expect(normalizeGitVersion("")).toBe(null);
expect(normalizeGitVersion(null)).toBe(null);
expect(normalizeGitVersion(undefined)).toBe(null);
});
it("strips the leading v on a clean tag", () => {
expect(normalizeGitVersion("v0.1.36")).toBe("0.1.36");
expect(normalizeGitVersion("v1.0.0")).toBe("1.0.0");
});
it("preserves the prerelease suffix between tags", () => {
expect(normalizeGitVersion("v0.1.35-14-gf1415e96")).toBe(
"0.1.35-14-gf1415e96",
);
});
it("preserves the dirty suffix on a modified worktree", () => {
expect(normalizeGitVersion("v0.1.35-14-gf1415e96-dirty")).toBe(
"0.1.35-14-gf1415e96-dirty",
);
});
it("handles v-prefixed prerelease tags", () => {
expect(normalizeGitVersion("v1.0.0-alpha")).toBe("1.0.0-alpha");
expect(normalizeGitVersion("v1.0.0-rc.2")).toBe("1.0.0-rc.2");
});
it("falls back to 0.0.0-<hash> when no tags are reachable", () => {
// `git describe --tags --always` returns just the short commit hash
// when there are no tags in the history at all.
expect(normalizeGitVersion("f1415e96")).toBe("0.0.0-f1415e96");
expect(normalizeGitVersion("abc1234")).toBe("0.0.0-abc1234");
});
});

View File

@@ -0,0 +1,173 @@
import { app } from "electron";
import { execFile } from "child_process";
import { createHash } from "crypto";
import { createReadStream, createWriteStream, existsSync } from "fs";
import { chmod, mkdir, rename, rm } from "fs/promises";
import { join, dirname } from "path";
import { pipeline } from "stream/promises";
import { tmpdir } from "os";
import { Readable } from "stream";
// Desktop bootstraps its own copy of the `multica` CLI into userData on first
// launch, so users never have to brew-install anything. Build-time decoupled:
// we don't bundle the binary into the .app, we download whatever the upstream
// release is at first run.
const GITHUB_LATEST_BASE =
"https://github.com/multica-ai/multica/releases/latest/download";
function platformAssetName(): string {
const osMap: Record<string, string> = {
darwin: "darwin",
linux: "linux",
win32: "windows",
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64",
};
const os = osMap[process.platform];
const arch = archMap[process.arch];
if (!os || !arch) {
throw new Error(
`unsupported platform for CLI auto-install: ${process.platform}/${process.arch}`,
);
}
const ext = process.platform === "win32" ? "zip" : "tar.gz";
return `multica_${os}_${arch}.${ext}`;
}
function binaryName(): string {
return process.platform === "win32" ? "multica.exe" : "multica";
}
export function managedCliPath(): string {
return join(app.getPath("userData"), "bin", binaryName());
}
function run(cmd: string, args: string[], cwd?: string): Promise<void> {
return new Promise((resolve, reject) => {
execFile(cmd, args, { cwd }, (err) => (err ? reject(err) : resolve()));
});
}
async function downloadToFile(url: string, dest: string): Promise<void> {
const res = await fetch(url, { redirect: "follow" });
if (!res.ok || !res.body) {
throw new Error(`download failed: ${res.status} ${res.statusText}`);
}
await mkdir(dirname(dest), { recursive: true });
// Node's fetch returns a web ReadableStream; adapt to a Node stream for pipeline.
const nodeStream = Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]);
await pipeline(nodeStream, createWriteStream(dest));
}
// Fetch goreleaser's published checksums.txt and parse it into a
// filename → sha256 lookup. Format is `<hex> <filename>` per line.
async function fetchChecksums(): Promise<Map<string, string>> {
const url = `${GITHUB_LATEST_BASE}/checksums.txt`;
const res = await fetch(url, { redirect: "follow" });
if (!res.ok) {
throw new Error(
`checksums.txt fetch failed: ${res.status} ${res.statusText}`,
);
}
const text = await res.text();
const map = new Map<string, string>();
for (const rawLine of text.split("\n")) {
const line = rawLine.trim();
if (!line) continue;
const match = line.match(/^([a-f0-9]{64})\s+\*?(\S+)$/i);
if (match) map.set(match[2], match[1].toLowerCase());
}
return map;
}
async function sha256OfFile(path: string): Promise<string> {
const hash = createHash("sha256");
await pipeline(createReadStream(path), hash);
return hash.digest("hex");
}
async function verifyChecksum(
archivePath: string,
assetName: string,
): Promise<void> {
const checksums = await fetchChecksums();
const expected = checksums.get(assetName);
if (!expected) {
throw new Error(
`no checksum for ${assetName} in checksums.txt — refusing to install unverified binary`,
);
}
const actual = await sha256OfFile(archivePath);
if (actual.toLowerCase() !== expected) {
throw new Error(
`checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`,
);
}
}
async function extractArchive(archive: string, dest: string): Promise<void> {
await mkdir(dest, { recursive: true });
// Modern OSes all ship a `tar` that auto-detects tar.gz and zip:
// - macOS/Linux: GNU tar or bsdtar
// - Windows 10+: bsdtar is bundled as `tar.exe` since build 17063
await run("tar", ["-xf", archive, "-C", dest]);
}
async function installFresh(): Promise<string> {
const target = managedCliPath();
const assetName = platformAssetName();
const url = `${GITHUB_LATEST_BASE}/${assetName}`;
const workDir = join(tmpdir(), `multica-cli-${Date.now()}`);
await mkdir(workDir, { recursive: true });
try {
const archivePath = join(workDir, assetName);
console.log(`[cli-bootstrap] downloading ${url}`);
await downloadToFile(url, archivePath);
console.log(`[cli-bootstrap] verifying ${assetName} against checksums.txt`);
await verifyChecksum(archivePath, assetName);
console.log(`[cli-bootstrap] extracting ${assetName}`);
await extractArchive(archivePath, workDir);
const extractedBin = join(workDir, binaryName());
if (!existsSync(extractedBin)) {
throw new Error(
`archive ${assetName} did not contain ${binaryName()} at its root`,
);
}
await mkdir(dirname(target), { recursive: true });
await rename(extractedBin, target);
await chmod(target, 0o755);
// macOS: ad-hoc sign so spawning the child never hits a gatekeeper quirk.
// Non-fatal: unsigned binaries still execute when the parent app is trusted.
if (process.platform === "darwin") {
await run("codesign", ["-s", "-", "--force", target]).catch((err) => {
console.warn("[cli-bootstrap] ad-hoc codesign failed:", err);
});
}
console.log(`[cli-bootstrap] installed CLI at ${target}`);
return target;
} finally {
await rm(workDir, { recursive: true, force: true }).catch(() => {});
}
}
/**
* Returns the path to a usable `multica` binary. If one is already present at
* the managed userData location, returns it immediately. Otherwise downloads
* the latest release asset for the current platform and installs it.
*/
export async function ensureManagedCli(): Promise<string> {
const target = managedCliPath();
if (existsSync(target)) return target;
return installFresh();
}

View File

@@ -0,0 +1,901 @@
import { app, ipcMain, BrowserWindow } from "electron";
import { execFile } from "child_process";
import {
readFile,
writeFile,
mkdir,
rm,
open,
stat,
} from "fs/promises";
import {
existsSync,
watchFile,
unwatchFile,
type StatsListener,
} from "fs";
import { join } from "path";
import { homedir } from "os";
import type { DaemonStatus, DaemonPrefs } from "../shared/daemon-types";
import { ensureManagedCli, managedCliPath } from "./cli-bootstrap";
import { decideVersionAction } from "./version-decision";
const DEFAULT_HEALTH_PORT = 19514;
const POLL_INTERVAL_MS = 5_000;
const PREFS_PATH = join(homedir(), ".multica", "desktop_prefs.json");
const LOG_TAIL_RETRY_MS = 2_000;
const LOG_TAIL_MAX_RETRIES = 5;
const DEFAULT_PREFS: DaemonPrefs = { autoStart: true, autoStop: false };
interface ActiveProfile {
name: string; // "" = default profile
port: number;
}
let statusPollTimer: ReturnType<typeof setInterval> | null = null;
let logTailWatcher: { path: string; listener: StatsListener } | null = null;
let currentState: DaemonStatus["state"] = "installing_cli";
let getMainWindow: () => BrowserWindow | null = () => null;
let operationInProgress = false;
let cachedCliBinary: string | null | undefined = undefined;
let cliResolvePromise: Promise<string | null> | null = null;
let cachedCliBinaryVersion: string | null | undefined = undefined;
// Set when a CLI version mismatch was detected but the running daemon is
// busy executing tasks. The poll loop retries the check on each tick and
// fires the restart once active_task_count drops to 0.
let pendingVersionRestart = false;
let targetApiBaseUrl: string | null = null;
let activeProfile: ActiveProfile | null = null;
// Serialize all writes to any profile config file. Multiple paths
// (syncToken, resolveActiveProfile, clearToken, watch/unwatch handlers)
// may try to write concurrently; chaining them avoids interleaved writes
// corrupting the JSON.
let configWriteChain: Promise<void> = Promise.resolve();
// Keep the Go impl in sync: server/cmd/multica/cmd_daemon.go healthPortForProfile.
function healthPortForProfile(profile: string): number {
if (!profile) return DEFAULT_HEALTH_PORT;
let sum = 0;
for (const b of Buffer.from(profile, "utf-8")) sum += b;
return DEFAULT_HEALTH_PORT + 1 + (sum % 1000);
}
function profileDir(profile: string): string {
return profile
? join(homedir(), ".multica", "profiles", profile)
: join(homedir(), ".multica");
}
function profileConfigPath(profile: string): string {
return join(profileDir(profile), "config.json");
}
function profileLogPath(profile: string): string {
return join(profileDir(profile), "daemon.log");
}
// Sidecar file that records which Multica user the cached PAT in config.json
// was minted for. The Go CLI/daemon never read or write this file, so it
// survives Go-side config rewrites. Used to detect user switches and mint a
// fresh PAT instead of reusing a token that belongs to a previous user.
function profileUserIdPath(profile: string): string {
return join(profileDir(profile), ".desktop-user-id");
}
async function readProfileUserId(profile: string): Promise<string | null> {
try {
const raw = await readFile(profileUserIdPath(profile), "utf-8");
const trimmed = raw.trim();
return trimmed || null;
} catch {
return null;
}
}
async function writeProfileUserId(
profile: string,
userId: string,
): Promise<void> {
await mkdir(profileDir(profile), { recursive: true });
await writeFile(profileUserIdPath(profile), userId, "utf-8");
}
async function removeProfileUserId(profile: string): Promise<void> {
try {
await rm(profileUserIdPath(profile));
} catch {
// Already gone — nothing to do.
}
}
function normalizeUrl(u: string): string {
if (!u) return "";
try {
const parsed = new URL(u);
return `${parsed.protocol}//${parsed.host}`.toLowerCase();
} catch {
return u.replace(/\/+$/, "").toLowerCase();
}
}
function urlsMatch(a: string, b: string): boolean {
const na = normalizeUrl(a);
const nb = normalizeUrl(b);
return na.length > 0 && na === nb;
}
function sendStatus(status: DaemonStatus): void {
const win = getMainWindow();
win?.webContents.send("daemon:status", status);
}
interface HealthPayload {
status?: string;
pid?: number;
uptime?: string;
daemon_id?: string;
device_name?: string;
server_url?: string;
cli_version?: string;
active_task_count?: number;
agents?: string[];
workspaces?: unknown[];
}
async function fetchHealthAtPort(
port: number,
): Promise<HealthPayload | null> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2_000);
const res = await fetch(`http://127.0.0.1:${port}/health`, {
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) return null;
return (await res.json()) as HealthPayload;
} catch {
return null;
}
}
// Desktop owns a dedicated CLI profile named after the target API host, so it
// never reads or writes the user's hand-configured profiles. Profile dir:
// ~/.multica/profiles/desktop-<host>/
function deriveProfileName(targetUrl: string): string {
try {
const url = new URL(targetUrl);
const host = url.host.replace(/:/g, "-").toLowerCase();
return `desktop-${host}`;
} catch {
return "desktop";
}
}
async function readProfileConfig(
profile: string,
): Promise<Record<string, unknown>> {
try {
const raw = await readFile(profileConfigPath(profile), "utf-8");
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
async function writeProfileConfig(
profile: string,
cfg: Record<string, unknown>,
): Promise<void> {
const op = async () => {
await mkdir(profileDir(profile), { recursive: true });
await writeFile(
profileConfigPath(profile),
JSON.stringify(cfg, null, 2),
"utf-8",
);
};
const next = configWriteChain.catch(() => {}).then(op);
configWriteChain = next.catch(() => {});
return next;
}
/**
* Returns the Desktop-owned profile for the current target API URL. Creates
* the profile's config.json on demand with `server_url` pinned to the target.
*
* This function never falls back to the default profile, and never touches a
* profile whose name doesn't start with `desktop-`, so the user's manually
* configured CLI profiles are untouched.
*/
async function resolveActiveProfile(): Promise<ActiveProfile> {
const target = targetApiBaseUrl;
if (!target) return { name: "", port: DEFAULT_HEALTH_PORT };
const name = deriveProfileName(target);
const cfg = await readProfileConfig(name);
if (cfg.server_url !== target) {
cfg.server_url = target;
await writeProfileConfig(name, cfg);
console.log(`[daemon] initialized profile "${name}" → ${target}`);
}
return { name, port: healthPortForProfile(name) };
}
async function ensureActiveProfile(): Promise<ActiveProfile> {
if (activeProfile) return activeProfile;
activeProfile = await resolveActiveProfile();
return activeProfile;
}
function invalidateActiveProfile(): void {
activeProfile = null;
}
async function fetchHealth(): Promise<DaemonStatus> {
// While the CLI is being downloaded or has permanently failed, short-circuit
// polling — there's nothing to probe yet and /health calls would just return
// "stopped", which would overwrite the correct setup state in the UI.
if (currentState === "installing_cli" || currentState === "cli_not_found") {
return { state: currentState };
}
const active = await ensureActiveProfile();
const data = await fetchHealthAtPort(active.port);
if (!data || data.status !== "running") {
return {
state: currentState === "starting" ? "starting" : "stopped",
profile: active.name,
};
}
// Safety: if we have a target URL and the daemon on our port reports a
// different server_url, it's not "our" daemon — drop it and re-resolve.
if (
targetApiBaseUrl &&
data.server_url &&
!urlsMatch(data.server_url, targetApiBaseUrl)
) {
invalidateActiveProfile();
return { state: "stopped" };
}
return {
state: "running",
pid: data.pid,
uptime: data.uptime,
daemonId: data.daemon_id,
deviceName: data.device_name,
agents: data.agents ?? [],
workspaceCount: Array.isArray(data.workspaces)
? data.workspaces.length
: 0,
profile: active.name,
serverUrl: data.server_url,
};
}
function findCliOnPath(): string | null {
const candidates = process.platform === "win32" ? ["multica.exe"] : ["multica"];
const paths = (process.env["PATH"] ?? "").split(
process.platform === "win32" ? ";" : ":",
);
if (process.platform === "darwin") {
paths.push("/opt/homebrew/bin", "/usr/local/bin");
}
for (const name of candidates) {
for (const dir of paths) {
const full = join(dir, name);
if (existsSync(full)) return full;
}
}
return null;
}
/**
* Returns the path to the CLI binary bundled inside the Desktop app.
*
* - Dev (`electron-vite dev`): `app.getAppPath()` → `apps/desktop`, resolving
* to `apps/desktop/resources/bin/multica`. `bundle-cli.mjs` populates this
* before dev starts, so iterating on Go changes is "make build → restart".
* - Packaged: `app.getAppPath()` → `<Multica.app>/Contents/Resources/app.asar`.
* electron-builder's `asarUnpack: resources/**` extracts the binary to
* `app.asar.unpacked/`, so we swap the path segment to execute it.
*/
function bundledCliPath(): string {
const binName = process.platform === "win32" ? "multica.exe" : "multica";
return join(app.getAppPath(), "resources", "bin", binName).replace(
"app.asar",
"app.asar.unpacked",
);
}
/**
* Returns a usable `multica` binary path. Priority:
* 1. Cached result from a previous successful resolve.
* 2. Bundled binary shipped with the Desktop app (`bundle-cli.mjs`).
* 3. Managed binary already installed in userData (`managedCliPath`).
* 4. Download + install latest release into userData.
* 5. `multica` on PATH (dev convenience / user-installed via brew).
* Returns `null` only when all of the above fail.
*
* Bundled is preferred so Desktop iterates in lockstep with Go changes in
* the same repo — avoids the 404 / stale-API problem when the Desktop's
* TS side is ahead of the last published CLI release.
*
* This function is idempotent and safe to call concurrently — in-flight
* installs are de-duplicated via `cliResolvePromise`.
*/
async function resolveCliBinary(): Promise<string | null> {
if (cachedCliBinary !== undefined) return cachedCliBinary;
if (cliResolvePromise) return cliResolvePromise;
cliResolvePromise = (async () => {
const bundled = bundledCliPath();
if (existsSync(bundled)) {
console.log(`[daemon] using bundled CLI at ${bundled}`);
cachedCliBinary = bundled;
return bundled;
}
const managed = managedCliPath();
if (existsSync(managed)) {
cachedCliBinary = managed;
return managed;
}
try {
const installed = await ensureManagedCli();
cachedCliBinary = installed;
return installed;
} catch (err) {
console.warn("[daemon] CLI auto-install failed, falling back to PATH:", err);
const onPath = findCliOnPath();
cachedCliBinary = onPath;
return onPath;
}
})();
try {
return await cliResolvePromise;
} finally {
cliResolvePromise = null;
}
}
/**
* Reads the version of the currently resolved CLI binary by invoking
* `multica version --output json`. Cached for the process lifetime — the
* bundled binary doesn't change after `bundle-cli.mjs` runs at dev/build time.
* Returns null on any failure (unknown `go` at bundle time, broken binary,
* etc.) so callers can fail open.
*/
async function getCliBinaryVersion(): Promise<string | null> {
if (cachedCliBinaryVersion !== undefined) return cachedCliBinaryVersion;
const bin = await resolveCliBinary();
if (!bin) {
cachedCliBinaryVersion = null;
return null;
}
try {
const stdout = await new Promise<string>((resolve, reject) => {
execFile(
bin,
["version", "--output", "json"],
{ timeout: 5_000 },
(err, out) => {
if (err) reject(err);
else resolve(out);
},
);
});
const parsed = JSON.parse(stdout) as { version?: string };
cachedCliBinaryVersion = parsed.version ?? null;
} catch (err) {
console.warn("[daemon] failed to read CLI binary version:", err);
cachedCliBinaryVersion = null;
}
return cachedCliBinaryVersion;
}
/**
* Compares the running daemon's `cli_version` against the CLI binary we
* would use to spawn a new one, and restarts only when safe. The decision
* logic itself is in `version-decision.ts` (pure, unit-tested); this
* wrapper handles the async plumbing and side effects.
*
* Restart is only fired when ALL of:
* - a daemon is actually running on the active profile's port
* - both sides report a version and the strings differ
* - `active_task_count` is 0 (no in-flight agent work would be killed)
*
* On a confirmed mismatch while the daemon is busy, `pendingVersionRestart`
* is set; the poll loop retries this function on each 5s tick and will fire
* the restart as soon as the daemon drains.
*/
async function ensureRunningDaemonVersionMatches(): Promise<
"restarted" | "deferred" | "ok" | "not_running"
> {
const active = await ensureActiveProfile();
const running = await fetchHealthAtPort(active.port);
const bundled = await getCliBinaryVersion();
const action = decideVersionAction(bundled, running);
switch (action) {
case "not_running":
pendingVersionRestart = false;
return "not_running";
case "ok":
pendingVersionRestart = false;
return "ok";
case "defer": {
if (!pendingVersionRestart) {
const activeTasks = running?.active_task_count ?? 0;
console.log(
`[daemon] CLI version mismatch (bundled=${bundled} running=${running?.cli_version}); deferring restart until ${activeTasks} active task(s) finish`,
);
}
pendingVersionRestart = true;
return "deferred";
}
case "restart":
console.log(
`[daemon] CLI version mismatch (bundled=${bundled} running=${running?.cli_version}) — restarting daemon`,
);
pendingVersionRestart = false;
await restartDaemon();
return "restarted";
}
}
/**
* Exchange the user's JWT for a long-lived PAT via POST /api/tokens. The
* daemon needs a PAT (or `mul_` / `mdt_` token) because JWTs expire in 30
* days and signatures are tied to a specific backend instance.
*/
async function mintPat(jwt: string): Promise<string> {
if (!targetApiBaseUrl) {
throw new Error("mint PAT: target API URL not set");
}
const url = `${targetApiBaseUrl.replace(/\/+$/, "")}/api/tokens`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwt}`,
},
// Omit expires_in_days → server treats as null → non-expiring PAT.
body: JSON.stringify({ name: "Multica Desktop" }),
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`mint PAT failed: ${res.status} ${res.statusText} ${body}`);
}
const data = (await res.json()) as { token?: unknown };
if (typeof data.token !== "string" || !data.token.startsWith("mul_")) {
throw new Error("mint PAT: response missing token");
}
return data.token;
}
/**
* Ensure the active profile's config.json has a usable token for the daemon.
*
* - Input from the renderer is the user's JWT (from localStorage) plus the
* current user's id, so we can detect session changes.
* - If the profile already has a cached PAT (`mul_...`) AND the sidecar user
* id matches the caller, reuse it — minting fresh on every launch would
* accumulate garbage in the user's tokens page.
* - On user mismatch (or first run) call POST /api/tokens with the JWT to
* mint a fresh PAT, overwriting any stale cached PAT. This is the critical
* path: without it, a previous user's PAT would be used by a new session.
* - If the caller happens to pass a PAT directly, write it through.
* - When we mint fresh and a daemon is already running, restart it so the
* new credentials take effect (the Go daemon reads config at startup).
*/
async function syncToken(
tokenFromRenderer: string,
userId: string,
): Promise<void> {
const active = await ensureActiveProfile();
const config = await readProfileConfig(active.name);
const previousUserId = await readProfileUserId(active.name);
const userChanged = Boolean(previousUserId) && previousUserId !== userId;
const sameUserWithCachedPat =
!userChanged &&
previousUserId === userId &&
typeof config.token === "string" &&
config.token.startsWith("mul_");
let finalToken: string;
if (tokenFromRenderer.startsWith("mul_")) {
finalToken = tokenFromRenderer;
} else if (sameUserWithCachedPat) {
finalToken = config.token as string;
} else {
try {
finalToken = await mintPat(tokenFromRenderer);
console.log(
`[daemon] minted PAT for profile "${active.name}" (user_changed=${userChanged})`,
);
} catch (err) {
console.error("[daemon] failed to mint PAT:", err);
throw err;
}
}
config.token = finalToken;
if (targetApiBaseUrl) config.server_url = targetApiBaseUrl;
await writeProfileConfig(active.name, config);
await writeProfileUserId(active.name, userId);
// If we just rotated credentials onto a running daemon, restart it so the
// in-memory token in the Go process matches the new config.
if (userChanged) {
try {
const existing = await fetchHealthAtPort(active.port);
if (existing?.status === "running") {
console.log(
"[daemon] user switched — restarting daemon with new credentials",
);
void restartDaemon();
}
} catch (err) {
console.warn("[daemon] restart-on-user-switch failed:", err);
}
}
}
async function loadPrefs(): Promise<DaemonPrefs> {
try {
const raw = await readFile(PREFS_PATH, "utf-8");
const parsed = JSON.parse(raw);
return { ...DEFAULT_PREFS, ...parsed };
} catch {
return { ...DEFAULT_PREFS };
}
}
async function savePrefs(prefs: DaemonPrefs): Promise<void> {
const dir = join(homedir(), ".multica");
await mkdir(dir, { recursive: true });
await writeFile(PREFS_PATH, JSON.stringify(prefs, null, 2), "utf-8");
}
async function clearToken(): Promise<void> {
const active = await ensureActiveProfile();
const config = await readProfileConfig(active.name);
if ("token" in config) {
delete config.token;
await writeProfileConfig(active.name, config);
}
// Always drop the sidecar so a subsequent syncToken from any user is
// treated as a fresh mint, not a reuse of a stale cached PAT.
await removeProfileUserId(active.name);
}
async function withGuard<T>(fn: () => Promise<T>): Promise<T | { success: false; error: string }> {
if (operationInProgress) {
return { success: false, error: "Another daemon operation is in progress" };
}
operationInProgress = true;
try {
return await fn();
} finally {
operationInProgress = false;
}
}
function profileArgs(active: ActiveProfile): string[] {
return active.name ? ["--profile", active.name] : [];
}
// Env passed to every CLI child so the daemon process knows it was spawned
// by the Desktop app. The server uses this to mark runtimes as managed and
// hide CLI self-update UI.
const DESKTOP_SPAWN_ENV = {
...process.env,
MULTICA_LAUNCHED_BY: "desktop",
};
async function startDaemon(): Promise<{ success: boolean; error?: string }> {
const bin = await resolveCliBinary();
if (!bin) return { success: false, error: "multica CLI is not installed" };
const active = await ensureActiveProfile();
const existing = await fetchHealthAtPort(active.port);
if (existing?.status === "running") {
pollOnce();
return { success: true };
}
currentState = "starting";
sendStatus({ state: "starting" });
const args = ["daemon", "start", ...profileArgs(active)];
return new Promise((resolve) => {
execFile(
bin,
args,
{ timeout: 20_000, env: DESKTOP_SPAWN_ENV },
(err) => {
if (err) {
currentState = "stopped";
sendStatus({ state: "stopped" });
resolve({ success: false, error: err.message });
return;
}
// Stay in "starting" until pollOnce confirms /health — the CLI
// returning 0 only means the supervisor was spawned, not that the
// daemon process is already listening.
pollOnce();
resolve({ success: true });
},
);
});
}
async function stopDaemon(): Promise<{ success: boolean; error?: string }> {
const bin = await resolveCliBinary();
if (!bin) return { success: false, error: "multica CLI is not installed" };
const active = await ensureActiveProfile();
currentState = "stopping";
sendStatus({ state: "stopping" });
const args = ["daemon", "stop", ...profileArgs(active)];
return new Promise((resolve) => {
execFile(bin, args, { timeout: 15_000 }, (err) => {
if (err) {
resolve({ success: false, error: err.message });
} else {
resolve({ success: true });
}
currentState = "stopped";
sendStatus({ state: "stopped" });
});
});
}
async function restartDaemon(): Promise<{ success: boolean; error?: string }> {
const stopResult = await stopDaemon();
if (!stopResult.success) return stopResult;
return startDaemon();
}
async function pollOnce(): Promise<void> {
const status = await fetchHealth();
currentState = status.state;
sendStatus(status);
// Retry a deferred version-mismatch restart once the daemon drains.
if (pendingVersionRestart && status.state === "running") {
void ensureRunningDaemonVersionMatches();
}
}
function startPolling(): void {
if (statusPollTimer) return;
pollOnce();
statusPollTimer = setInterval(pollOnce, POLL_INTERVAL_MS);
}
/**
* Ensures the CLI binary is available, then transitions into the normal
* stopped/running state machine. Called once at startup and again on
* user-triggered `daemon:retry-install`.
*/
async function bootstrapCli(): Promise<void> {
const bin = await resolveCliBinary();
if (!bin) {
currentState = "cli_not_found";
sendStatus({ state: "cli_not_found" });
return;
}
currentState = "stopped";
sendStatus({ state: "stopped" });
startPolling();
}
function stopPolling(): void {
if (statusPollTimer) {
clearInterval(statusPollTimer);
statusPollTimer = null;
}
}
const LOG_TAIL_INITIAL_WINDOW_BYTES = 32 * 1024;
const LOG_TAIL_INITIAL_LINES = 200;
const LOG_TAIL_POLL_MS = 500;
async function readLogRange(
path: string,
startAt: number,
length: number,
): Promise<string> {
const handle = await open(path, "r");
try {
const buffer = Buffer.alloc(length);
const { bytesRead } = await handle.read(buffer, 0, length, startAt);
return buffer.subarray(0, bytesRead).toString("utf-8");
} finally {
await handle.close();
}
}
function sendLines(win: BrowserWindow, text: string): void {
const lines = text.split("\n").filter((line) => line.length > 0);
for (const line of lines) {
win.webContents.send("daemon:log-line", line);
}
}
// Cross-platform tail -f replacement: read the tail of the file once, then
// poll its stat with fs.watchFile and forward any new bytes since the last
// known offset. watchFile works on macOS, Linux, and Windows; spawn("tail")
// would silently fail on Windows.
function startLogTail(win: BrowserWindow, retryCount = 0): void {
stopLogTail();
void ensureActiveProfile().then(async (active) => {
const logPath = profileLogPath(active.name);
if (!existsSync(logPath)) {
if (retryCount < LOG_TAIL_MAX_RETRIES) {
setTimeout(() => startLogTail(win, retryCount + 1), LOG_TAIL_RETRY_MS);
}
return;
}
let position = 0;
try {
const initialStats = await stat(logPath);
const windowBytes = Math.min(
initialStats.size,
LOG_TAIL_INITIAL_WINDOW_BYTES,
);
const startAt = initialStats.size - windowBytes;
if (windowBytes > 0) {
const text = await readLogRange(logPath, startAt, windowBytes);
const lines = text
.split("\n")
.filter((line) => line.length > 0)
.slice(-LOG_TAIL_INITIAL_LINES);
for (const line of lines) {
win.webContents.send("daemon:log-line", line);
}
}
position = initialStats.size;
} catch (err) {
console.warn("[daemon] log tail initial read failed:", err);
return;
}
const listener: StatsListener = (curr) => {
const target = getMainWindow();
if (!target) return;
// File rotated/truncated — restart from the new beginning.
if (curr.size < position) position = 0;
if (curr.size === position) return;
const from = position;
const length = curr.size - from;
position = curr.size;
readLogRange(logPath, from, length)
.then((text) => sendLines(target, text))
.catch((err) => {
console.warn("[daemon] log tail read failed:", err);
});
};
watchFile(logPath, { interval: LOG_TAIL_POLL_MS }, listener);
logTailWatcher = { path: logPath, listener };
});
}
function stopLogTail(): void {
if (logTailWatcher) {
unwatchFile(logTailWatcher.path, logTailWatcher.listener);
logTailWatcher = null;
}
}
export function setupDaemonManager(
windowGetter: () => BrowserWindow | null,
): void {
getMainWindow = windowGetter;
ipcMain.handle("daemon:set-target-api-url", async (_e, url: string) => {
const normalized = url || null;
if (targetApiBaseUrl !== normalized) {
console.log(`[daemon] target API URL set to ${normalized ?? "(none)"}`);
targetApiBaseUrl = normalized;
invalidateActiveProfile();
await pollOnce();
}
});
ipcMain.handle("daemon:start", () => withGuard(() => startDaemon()));
ipcMain.handle("daemon:stop", () => withGuard(() => stopDaemon()));
ipcMain.handle("daemon:restart", () => withGuard(() => restartDaemon()));
ipcMain.handle("daemon:get-status", () => fetchHealth());
ipcMain.handle(
"daemon:sync-token",
(_event, token: string, userId: string) => syncToken(token, userId),
);
ipcMain.handle("daemon:clear-token", () => clearToken());
ipcMain.handle("daemon:is-cli-installed", async () => {
const bin = await resolveCliBinary();
return bin !== null;
});
ipcMain.handle("daemon:retry-install", async () => {
cachedCliBinary = undefined;
cliResolvePromise = null;
// A retry-install may land a new CLI at a different version; drop the
// cached version string so the next check re-reads the binary.
cachedCliBinaryVersion = undefined;
await bootstrapCli();
});
ipcMain.handle("daemon:get-prefs", () => loadPrefs());
ipcMain.handle(
"daemon:set-prefs",
(_event, prefs: Partial<DaemonPrefs>) =>
loadPrefs().then((cur) => {
const merged = { ...cur, ...prefs };
return savePrefs(merged).then(() => merged);
}),
);
ipcMain.handle("daemon:auto-start", async () => {
const prefs = await loadPrefs();
if (!prefs.autoStart) return;
const bin = await resolveCliBinary();
if (!bin) return;
const health = await fetchHealth();
if (health.state === "running") {
// Daemon is up but may be running an older CLI than the one we just
// bundled. Restart it so the new binary actually takes effect.
await ensureRunningDaemonVersionMatches();
return;
}
await startDaemon();
});
ipcMain.on("daemon:start-log-stream", () => {
const win = getMainWindow();
if (win) startLogTail(win);
});
ipcMain.on("daemon:stop-log-stream", () => {
stopLogTail();
});
// First-run CLI install kicks off here. Status bar shows "Setting up…"
// until the managed binary is on disk (instant on subsequent launches).
currentState = "installing_cli";
sendStatus({ state: "installing_cli" });
void bootstrapCli();
let isQuitting = false;
app.on("before-quit", (event) => {
if (isQuitting) return;
stopPolling();
stopLogTail();
loadPrefs().then(async (prefs) => {
if (prefs.autoStop) {
isQuitting = true;
event.preventDefault();
try {
await stopDaemon();
} catch {
// Best-effort stop on quit
}
app.quit();
}
});
});
}

View File

@@ -0,0 +1,155 @@
import { app, shell, BrowserWindow, ipcMain } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { setupAutoUpdater } from "./updater";
import { setupDaemonManager } from "./daemon-manager";
const PROTOCOL = "multica";
let mainWindow: BrowserWindow | null = null;
// --- Deep link helpers ---------------------------------------------------
function handleDeepLink(url: string): void {
try {
const parsed = new URL(url);
if (parsed.protocol !== `${PROTOCOL}:`) return;
// multica://auth/callback?token=<jwt>
if (parsed.hostname === "auth" && parsed.pathname === "/callback") {
const token = parsed.searchParams.get("token");
if (token && mainWindow) {
mainWindow.webContents.send("auth:token", token);
}
}
} catch {
// Ignore malformed URLs
}
}
// --- Window creation -----------------------------------------------------
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,
},
});
// Strip Origin header from WebSocket upgrade requests so the server's
// origin whitelist doesn't reject connections from localhost dev origins.
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
{ urls: ["wss://*/*", "ws://*/*"] },
(details, callback) => {
delete details.requestHeaders["Origin"];
callback({ requestHeaders: details.requestHeaders });
},
);
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"));
}
}
// --- Protocol registration -----------------------------------------------
if (process.defaultApp) {
// In dev, register with the path to the electron binary + app path
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [
app.getAppPath(),
]);
} else {
app.setAsDefaultProtocolClient(PROTOCOL);
}
// --- Single instance lock ------------------------------------------------
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
// Windows/Linux: second instance passes deep link via argv
app.on("second-instance", (_event, argv) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
// On Windows the deep link URL is the last argv entry
const deepLinkUrl = argv.find((arg) => arg.startsWith(`${PROTOCOL}://`));
if (deepLinkUrl) handleDeepLink(deepLinkUrl);
});
app.whenReady().then(() => {
electronApp.setAppUserModelId("ai.multica.desktop");
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// IPC: open URL in default browser (used by renderer for Google login)
ipcMain.handle("shell:openExternal", (_event, url: string) => {
return shell.openExternal(url);
});
// IPC: toggle immersive mode — hides the macOS traffic lights so full-screen
// modals (create-workspace, onboarding) can place UI in the top-left corner
// without fighting the native window controls' hit-test.
ipcMain.handle("window:setImmersive", (_event, immersive: boolean) => {
if (process.platform !== "darwin") return;
mainWindow?.setWindowButtonVisibility(!immersive);
});
createWindow();
setupAutoUpdater(() => mainWindow);
setupDaemonManager(() => mainWindow);
// macOS: deep link arrives via open-url event
app.on("open-url", (_event, url) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
handleDeepLink(url);
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Check argv for deep link on cold start (Windows/Linux)
const deepLinkArg = process.argv.find((arg) =>
arg.startsWith(`${PROTOCOL}://`),
);
if (deepLinkArg) {
app.whenReady().then(() => handleDeepLink(deepLinkArg));
}
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

View File

@@ -0,0 +1,46 @@
import { autoUpdater } from "electron-updater";
import { BrowserWindow, ipcMain } from "electron";
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
export function setupAutoUpdater(getMainWindow: () => BrowserWindow | null): void {
autoUpdater.on("update-available", (info) => {
const win = getMainWindow();
win?.webContents.send("updater:update-available", {
version: info.version,
releaseNotes: info.releaseNotes,
});
});
autoUpdater.on("download-progress", (progress) => {
const win = getMainWindow();
win?.webContents.send("updater:download-progress", {
percent: progress.percent,
});
});
autoUpdater.on("update-downloaded", () => {
const win = getMainWindow();
win?.webContents.send("updater:update-downloaded");
});
autoUpdater.on("error", (err) => {
console.error("Auto-updater error:", err);
});
ipcMain.handle("updater:download", () => {
return autoUpdater.downloadUpdate();
});
ipcMain.handle("updater:install", () => {
autoUpdater.quitAndInstall(false, true);
});
// Check for updates after a short delay to avoid blocking startup
setTimeout(() => {
autoUpdater.checkForUpdates().catch((err) => {
console.error("Failed to check for updates:", err);
});
}, 5000);
}

View File

@@ -0,0 +1,88 @@
import { describe, it, expect } from "vitest";
import { decideVersionAction } from "./version-decision";
describe("decideVersionAction", () => {
it("returns not_running when health payload is null", () => {
expect(decideVersionAction("v1.0.0", null)).toBe("not_running");
});
it("returns not_running when status is not 'running'", () => {
expect(
decideVersionAction("v1.0.0", { status: "stopped", cli_version: "v1.0.0" }),
).toBe("not_running");
});
it("returns ok when bundled version is unknown (fail safe)", () => {
expect(
decideVersionAction(null, {
status: "running",
cli_version: "v1.0.0",
active_task_count: 0,
}),
).toBe("ok");
});
it("returns ok when running daemon does not report cli_version (older daemon)", () => {
expect(
decideVersionAction("v1.0.0", {
status: "running",
active_task_count: 0,
}),
).toBe("ok");
});
it("returns ok when versions match exactly", () => {
expect(
decideVersionAction("v1.2.3", {
status: "running",
cli_version: "v1.2.3",
active_task_count: 5,
}),
).toBe("ok");
});
it("returns restart when versions differ and daemon is idle", () => {
expect(
decideVersionAction("v1.2.3", {
status: "running",
cli_version: "v1.2.2",
active_task_count: 0,
}),
).toBe("restart");
});
it("treats missing active_task_count as 0 (old daemon that still reports cli_version)", () => {
expect(
decideVersionAction("v1.2.3", {
status: "running",
cli_version: "v1.2.2",
}),
).toBe("restart");
});
it("returns defer when versions differ but daemon is busy", () => {
expect(
decideVersionAction("v1.2.3", {
status: "running",
cli_version: "v1.2.2",
active_task_count: 2,
}),
).toBe("defer");
});
it("transitions defer → restart as tasks drain", () => {
// Same bundled version across three observations while the daemon ages.
const bundled = "v2.0.0";
const base = { status: "running", cli_version: "v1.9.0" } as const;
expect(
decideVersionAction(bundled, { ...base, active_task_count: 3 }),
).toBe("defer");
expect(
decideVersionAction(bundled, { ...base, active_task_count: 1 }),
).toBe("defer");
expect(
decideVersionAction(bundled, { ...base, active_task_count: 0 }),
).toBe("restart");
});
});

View File

@@ -0,0 +1,37 @@
// Pure decision logic for the daemon version-check flow. Kept in its own
// module so it can be unit-tested without mocking Electron, execFile, or
// the HTTP health probe.
export interface VersionCheckHealth {
status?: string;
cli_version?: string;
active_task_count?: number;
}
export type VersionAction = "restart" | "defer" | "ok" | "not_running";
/**
* Decides what the daemon-manager should do given the currently-resolved
* bundled CLI version and the latest /health payload.
*
* not_running: no daemon is up, nothing to do
* ok: versions match, OR either side is unknown (fail safe)
* defer: versions differ but the daemon is busy — wait for drain
* restart: versions differ and the daemon is idle — safe to restart
*
* Pure function: no I/O, no side effects, no module state.
*/
export function decideVersionAction(
bundled: string | null,
running: VersionCheckHealth | null,
): VersionAction {
if (!running || running.status !== "running") return "not_running";
const runningVersion = running.cli_version;
if (!bundled || !runningVersion) return "ok";
if (runningVersion === bundled) return "ok";
const activeTasks = running.active_task_count ?? 0;
if (activeTasks > 0) return "defer";
return "restart";
}

65
apps/desktop/src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,65 @@
import { ElectronAPI } from "@electron-toolkit/preload";
interface DesktopAPI {
/** Listen for auth token delivered via deep link. Returns an unsubscribe function. */
onAuthToken: (callback: (token: string) => void) => () => void;
/** Open a URL in the default browser. */
openExternal: (url: string) => Promise<void>;
/** Hide macOS traffic lights for full-screen modals; restore when false. */
setImmersiveMode: (immersive: boolean) => Promise<void>;
}
interface DaemonStatus {
state: "running" | "stopped" | "starting" | "stopping" | "installing_cli" | "cli_not_found";
pid?: number;
uptime?: string;
daemonId?: string;
deviceName?: string;
agents?: string[];
workspaceCount?: number;
profile?: string;
serverUrl?: string;
}
interface DaemonPrefs {
autoStart: boolean;
autoStop: boolean;
}
interface DaemonAPI {
start: () => Promise<{ success: boolean; error?: string }>;
stop: () => Promise<{ success: boolean; error?: string }>;
restart: () => Promise<{ success: boolean; error?: string }>;
getStatus: () => Promise<DaemonStatus>;
onStatusChange: (callback: (status: DaemonStatus) => void) => () => void;
setTargetApiUrl: (url: string) => Promise<void>;
syncToken: (token: string, userId: string) => Promise<void>;
clearToken: () => Promise<void>;
isCliInstalled: () => Promise<boolean>;
getPrefs: () => Promise<DaemonPrefs>;
setPrefs: (prefs: Partial<DaemonPrefs>) => Promise<DaemonPrefs>;
autoStart: () => Promise<void>;
retryInstall: () => Promise<void>;
startLogStream: () => void;
stopLogStream: () => void;
onLogLine: (callback: (line: string) => void) => () => void;
}
interface UpdaterAPI {
onUpdateAvailable: (callback: (info: { version: string; releaseNotes?: string }) => void) => () => void;
onDownloadProgress: (callback: (progress: { percent: number }) => void) => () => void;
onUpdateDownloaded: (callback: () => void) => () => void;
downloadUpdate: () => Promise<void>;
installUpdate: () => Promise<void>;
}
declare global {
interface Window {
electron: ElectronAPI;
desktopAPI: DesktopAPI;
daemonAPI: DaemonAPI;
updater: UpdaterAPI;
}
}
export {};

View File

@@ -0,0 +1,106 @@
import { contextBridge, ipcRenderer } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
const desktopAPI = {
/** Listen for auth token delivered via deep link */
onAuthToken: (callback: (token: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, token: string) =>
callback(token);
ipcRenderer.on("auth:token", handler);
return () => {
ipcRenderer.removeListener("auth:token", handler);
};
},
/** Open a URL in the default browser */
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
/** Toggle immersive mode — hide macOS traffic lights for full-screen modals */
setImmersiveMode: (immersive: boolean) =>
ipcRenderer.invoke("window:setImmersive", immersive),
};
interface DaemonStatus {
state: "running" | "stopped" | "starting" | "stopping" | "installing_cli" | "cli_not_found";
pid?: number;
uptime?: string;
daemonId?: string;
deviceName?: string;
agents?: string[];
workspaceCount?: number;
profile?: string;
serverUrl?: string;
}
const daemonAPI = {
start: (): Promise<{ success: boolean; error?: string }> =>
ipcRenderer.invoke("daemon:start"),
stop: (): Promise<{ success: boolean; error?: string }> =>
ipcRenderer.invoke("daemon:stop"),
restart: (): Promise<{ success: boolean; error?: string }> =>
ipcRenderer.invoke("daemon:restart"),
getStatus: (): Promise<DaemonStatus> =>
ipcRenderer.invoke("daemon:get-status"),
onStatusChange: (callback: (status: DaemonStatus) => void) => {
const handler = (_: unknown, status: DaemonStatus) => callback(status);
ipcRenderer.on("daemon:status", handler);
return () => ipcRenderer.removeListener("daemon:status", handler);
},
setTargetApiUrl: (url: string): Promise<void> =>
ipcRenderer.invoke("daemon:set-target-api-url", url),
syncToken: (token: string, userId: string): Promise<void> =>
ipcRenderer.invoke("daemon:sync-token", token, userId),
clearToken: (): Promise<void> =>
ipcRenderer.invoke("daemon:clear-token"),
isCliInstalled: (): Promise<boolean> =>
ipcRenderer.invoke("daemon:is-cli-installed"),
getPrefs: (): Promise<{ autoStart: boolean; autoStop: boolean }> =>
ipcRenderer.invoke("daemon:get-prefs"),
setPrefs: (prefs: Partial<{ autoStart: boolean; autoStop: boolean }>): Promise<{ autoStart: boolean; autoStop: boolean }> =>
ipcRenderer.invoke("daemon:set-prefs", prefs),
autoStart: (): Promise<void> =>
ipcRenderer.invoke("daemon:auto-start"),
retryInstall: (): Promise<void> =>
ipcRenderer.invoke("daemon:retry-install"),
startLogStream: () => ipcRenderer.send("daemon:start-log-stream"),
stopLogStream: () => ipcRenderer.send("daemon:stop-log-stream"),
onLogLine: (callback: (line: string) => void) => {
const handler = (_: unknown, line: string) => callback(line);
ipcRenderer.on("daemon:log-line", handler);
return () => ipcRenderer.removeListener("daemon:log-line", handler);
},
};
const updaterAPI = {
onUpdateAvailable: (callback: (info: { version: string; releaseNotes?: string }) => void) => {
const handler = (_: unknown, info: { version: string; releaseNotes?: string }) => callback(info);
ipcRenderer.on("updater:update-available", handler);
return () => ipcRenderer.removeListener("updater:update-available", handler);
},
onDownloadProgress: (callback: (progress: { percent: number }) => void) => {
const handler = (_: unknown, progress: { percent: number }) => callback(progress);
ipcRenderer.on("updater:download-progress", handler);
return () => ipcRenderer.removeListener("updater:download-progress", handler);
},
onUpdateDownloaded: (callback: () => void) => {
const handler = () => callback();
ipcRenderer.on("updater:update-downloaded", handler);
return () => ipcRenderer.removeListener("updater:update-downloaded", handler);
},
downloadUpdate: () => ipcRenderer.invoke("updater:download"),
installUpdate: () => ipcRenderer.invoke("updater:install"),
};
if (process.contextIsolated) {
contextBridge.exposeInMainWorld("electron", electronAPI);
contextBridge.exposeInMainWorld("desktopAPI", desktopAPI);
contextBridge.exposeInMainWorld("daemonAPI", daemonAPI);
contextBridge.exposeInMainWorld("updater", updaterAPI);
} else {
// @ts-expect-error - fallback for non-isolated context
window.electron = electronAPI;
// @ts-expect-error - fallback for non-isolated context
window.desktopAPI = desktopAPI;
// @ts-expect-error - fallback for non-isolated context
window.daemonAPI = daemonAPI;
// @ts-expect-error - fallback for non-isolated context
window.updater = updaterAPI;
}

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,99 @@
import { useEffect } from "react";
import { CoreProvider } from "@multica/core/platform";
import { useAuthStore } from "@multica/core/auth";
import { useWorkspaceStore } from "@multica/core/workspace";
import { api } from "@multica/core/api";
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";
import { UpdateNotification } from "./components/update-notification";
function AppContent() {
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
// Tell the main process which backend URL we talk to, so daemon-manager
// can pick the matching CLI profile (server_url from ~/.multica config).
useEffect(() => {
window.daemonAPI.setTargetApiUrl(DAEMON_TARGET_API_URL);
}, []);
// Listen for auth token delivered via deep link (multica://auth/callback?token=...)
useEffect(() => {
return window.desktopAPI.onAuthToken(async (token) => {
try {
const loggedIn = await useAuthStore.getState().loginWithToken(token);
await window.daemonAPI.syncToken(token, loggedIn.id);
const wsList = await api.listWorkspaces();
const lastWsId = localStorage.getItem("multica_workspace_id");
useWorkspaceStore.getState().hydrateWorkspace(wsList, lastWsId);
} catch {
// Token invalid or expired — user stays on login page
}
});
}, []);
// Sync token and start the daemon whenever the user logs in.
useEffect(() => {
if (!user) return;
const token = localStorage.getItem("multica_token");
if (!token) return;
const userId = user.id;
(async () => {
try {
await window.daemonAPI.syncToken(token, userId);
await window.daemonAPI.autoStart();
} catch (err) {
console.error("Failed to sync daemon on login", err);
}
})();
}, [user]);
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 />;
}
// Backend the daemon should connect to — same URL the renderer talks to.
const DAEMON_TARGET_API_URL =
import.meta.env.VITE_API_URL || "http://localhost:8080";
// On logout, clear any cached PAT and stop the daemon so that a subsequent
// login as a different user never inherits the previous user's credentials.
async function handleDaemonLogout() {
try {
await window.daemonAPI.clearToken();
} catch {
// Best-effort — clearing is followed by stop which also hardens state.
}
try {
await window.daemonAPI.stop();
} catch {
// Daemon may already be stopped.
}
}
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"}
onLogout={handleDaemonLogout}
>
<AppContent />
</CoreProvider>
<Toaster />
<UpdateNotification />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,309 @@
import { useState, useEffect, useRef, useCallback } from "react";
import {
Play,
Square,
RotateCw,
Server,
ChevronDown,
X,
} from "lucide-react";
import { cn } from "@multica/ui/lib/utils";
import { Button } from "@multica/ui/components/ui/button";
import { toast } from "sonner";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@multica/ui/components/ui/sheet";
import type { DaemonStatus, DaemonState } from "../../../shared/daemon-types";
import { DAEMON_STATE_COLORS, DAEMON_STATE_LABELS } from "../../../shared/daemon-types";
interface DaemonPanelProps {
open: boolean;
onOpenChange: (open: boolean) => void;
status: DaemonStatus;
}
const LOG_LEVEL_COLORS: Record<string, string> = {
INFO: "text-info",
WARN: "text-warning",
ERROR: "text-destructive",
DEBUG: "text-muted-foreground",
};
function colorizeLogLine(line: string): { level: string; className: string } {
for (const [level, className] of Object.entries(LOG_LEVEL_COLORS)) {
if (line.includes(level)) return { level, className };
}
return { level: "", className: "text-muted-foreground" };
}
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-baseline justify-between gap-4 py-1">
<span className="shrink-0 text-xs text-muted-foreground">{label}</span>
<span className="truncate text-right text-sm">{value}</span>
</div>
);
}
function StatusDot({ state }: { state: DaemonState }) {
return <span className={cn("inline-block size-2 rounded-full", DAEMON_STATE_COLORS[state])} />;
}
interface LogEntry {
id: number;
line: string;
}
const MAX_LOG_LINES = 500;
let logIdCounter = 0;
export function DaemonPanel({ open, onOpenChange, status }: DaemonPanelProps) {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [autoScroll, setAutoScroll] = useState(true);
const [actionLoading, setActionLoading] = useState(false);
const logContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
window.daemonAPI.startLogStream();
const unsub = window.daemonAPI.onLogLine((line) => {
setLogs((prev) => {
const next = [...prev, { id: ++logIdCounter, line }];
return next.length > MAX_LOG_LINES ? next.slice(-MAX_LOG_LINES) : next;
});
});
return () => {
unsub();
window.daemonAPI.stopLogStream();
};
}, [open]);
useEffect(() => {
if (autoScroll && logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
}
}, [logs, autoScroll]);
const handleLogScroll = useCallback(() => {
const el = logContainerRef.current;
if (!el) return;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
setAutoScroll(atBottom);
}, []);
const scrollToBottom = useCallback(() => {
if (logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
setAutoScroll(true);
}
}, []);
const handleStart = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.start();
setActionLoading(false);
if (!result.success) {
toast.error("Failed to start daemon", { description: result.error });
}
}, []);
const handleStop = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.stop();
setActionLoading(false);
if (!result.success) {
toast.error("Failed to stop daemon", { description: result.error });
}
}, []);
const handleRestart = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.restart();
setActionLoading(false);
if (!result.success) {
toast.error("Failed to restart daemon", { description: result.error });
}
}, []);
const isTransitioning = status.state === "starting" || status.state === "stopping";
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className="flex flex-col sm:max-w-md"
showCloseButton={false}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
<SheetHeader className="flex-row items-center justify-between gap-2 pr-3">
<SheetTitle className="flex items-center gap-2">
<Server className="size-4" />
Local Daemon
</SheetTitle>
<button
type="button"
onClick={() => onOpenChange(false)}
aria-label="Close"
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<X className="size-4" />
</button>
</SheetHeader>
<div className="flex-1 min-h-0 flex flex-col gap-4 px-4">
<div className="shrink-0 space-y-4">
{/* Status info */}
<div className="rounded-lg border p-3 space-y-0.5">
<InfoRow
label="Status"
value={
<span className="flex items-center gap-1.5">
<StatusDot state={status.state} />
{DAEMON_STATE_LABELS[status.state]}
</span>
}
/>
{status.uptime && <InfoRow label="Uptime" value={status.uptime} />}
<InfoRow label="Profile" value={status.profile || "default"} />
{status.serverUrl && (
<InfoRow
label="Server"
value={
<span className="font-mono text-xs" title={status.serverUrl}>
{status.serverUrl}
</span>
}
/>
)}
{status.agents && status.agents.length > 0 && (
<InfoRow label="Agents" value={status.agents.join(", ")} />
)}
{status.deviceName && <InfoRow label="Device" value={status.deviceName} />}
{status.daemonId && (
<InfoRow
label="Daemon ID"
value={<span className="font-mono text-xs">{status.daemonId}</span>}
/>
)}
{typeof status.workspaceCount === "number" && (
<InfoRow label="Workspaces" value={status.workspaceCount} />
)}
{status.pid && (
<InfoRow
label="PID"
value={<span className="font-mono text-xs">{status.pid}</span>}
/>
)}
</div>
{/* Actions */}
{status.state === "installing_cli" ? (
<div className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground">
Setting up the local runtime this only happens the first time.
</div>
) : status.state === "cli_not_found" ? (
<div className="rounded-lg border border-destructive/40 bg-destructive/5 p-3 space-y-2">
<p className="text-sm">
Couldn&apos;t download the local runtime. Check your network
connection and try again.
</p>
<Button
size="sm"
variant="outline"
onClick={async () => {
setActionLoading(true);
try {
await window.daemonAPI.retryInstall();
} finally {
setActionLoading(false);
}
}}
disabled={actionLoading}
>
<RotateCw className="size-3.5 mr-1.5" />
Retry
</Button>
</div>
) : (
<div className="flex gap-2">
{status.state === "stopped" ? (
<Button size="sm" onClick={handleStart} disabled={actionLoading}>
<Play className="size-3.5 mr-1.5" />
Start
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={handleStop}
disabled={actionLoading || isTransitioning}
>
<Square className="size-3.5 mr-1.5" />
Stop
</Button>
<Button
variant="outline"
size="sm"
onClick={handleRestart}
disabled={actionLoading || isTransitioning}
>
<RotateCw className="size-3.5 mr-1.5" />
Restart
</Button>
</>
)}
</div>
)}
</div>
{/* Logs — fills remaining vertical space down to the sheet bottom */}
<div className="flex-1 min-h-0 flex flex-col gap-2 pb-4">
<div className="flex items-center justify-between shrink-0">
<h3 className="text-sm font-medium">Logs</h3>
{!autoScroll && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={scrollToBottom}
>
<ChevronDown className="size-3 mr-1" />
Scroll to bottom
</Button>
)}
</div>
<div
ref={logContainerRef}
onScroll={handleLogScroll}
className="flex-1 min-h-0 overflow-y-auto rounded-lg border bg-muted/30 p-2 font-mono text-xs leading-relaxed"
>
{logs.length === 0 ? (
<p className="text-muted-foreground/50 text-center py-8">
{status.state === "running"
? "Waiting for logs…"
: "Start the daemon to see logs"}
</p>
) : (
logs.map((entry) => {
const { className } = colorizeLogLine(entry.line);
return (
<div key={entry.id} className={cn("whitespace-pre-wrap break-all", className)}>
{entry.line}
</div>
);
})
)}
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,155 @@
import { useState, useEffect, useCallback } from "react";
import {
Play,
Square,
RotateCw,
Server,
Activity,
} from "lucide-react";
import { cn } from "@multica/ui/lib/utils";
import { Button } from "@multica/ui/components/ui/button";
import { toast } from "sonner";
import { DaemonPanel } from "./daemon-panel";
import type { DaemonStatus } from "../../../shared/daemon-types";
import { DAEMON_STATE_COLORS, DAEMON_STATE_LABELS, formatUptime } from "../../../shared/daemon-types";
export function DaemonRuntimeCard() {
const [status, setStatus] = useState<DaemonStatus>({ state: "stopped" });
const [panelOpen, setPanelOpen] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
useEffect(() => {
window.daemonAPI.getStatus().then((s) => setStatus(s));
const unsub = window.daemonAPI.onStatusChange((s) => {
setStatus(s);
setActionLoading(false);
});
return unsub;
}, []);
const handleStart = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.start();
if (!result.success) {
setActionLoading(false);
toast.error("Failed to start daemon", { description: result.error });
}
}, []);
const handleStop = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.stop();
if (!result.success) {
toast.error("Failed to stop daemon", { description: result.error });
}
}, []);
const handleRestart = useCallback(async () => {
setActionLoading(true);
const result = await window.daemonAPI.restart();
if (!result.success) {
toast.error("Failed to restart daemon", { description: result.error });
}
}, []);
const isTransitioning = status.state === "starting" || status.state === "stopping";
const isRunning = status.state === "running";
const isStopped = status.state === "stopped" || status.state === "cli_not_found";
const stopPropagation = (e: React.MouseEvent) => e.stopPropagation();
return (
<>
<div
role="button"
tabIndex={0}
onClick={() => setPanelOpen(true)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setPanelOpen(true);
}
}}
className="border-b px-4 py-3 cursor-pointer transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:bg-muted/40"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-lg bg-muted">
<Server className="size-4 text-muted-foreground" />
</div>
<div>
<h3 className="text-sm font-medium">Local Daemon</h3>
<div className="flex items-center gap-1.5 mt-0.5">
<span className={cn("size-1.5 rounded-full", DAEMON_STATE_COLORS[status.state])} />
<span className="text-xs text-muted-foreground">{DAEMON_STATE_LABELS[status.state]}</span>
{isRunning && status.uptime && (
<>
<span className="text-xs text-muted-foreground">·</span>
<span className="text-xs text-muted-foreground">{formatUptime(status.uptime)}</span>
</>
)}
{isRunning && status.agents && status.agents.length > 0 && (
<>
<span className="text-xs text-muted-foreground">·</span>
<span className="text-xs text-muted-foreground">{status.agents.join(", ")}</span>
</>
)}
</div>
</div>
</div>
<div
className="flex items-center gap-1.5 shrink-0"
onClick={stopPropagation}
>
{isStopped && (
<Button
size="sm"
variant="outline"
onClick={handleStart}
disabled={actionLoading || status.state === "cli_not_found"}
>
{actionLoading ? (
<Activity className="size-3.5 mr-1.5 animate-pulse" />
) : (
<Play className="size-3.5 mr-1.5" />
)}
Start
</Button>
)}
{isRunning && (
<>
<Button
size="sm"
variant="ghost"
onClick={handleRestart}
disabled={actionLoading}
>
<RotateCw className="size-3.5 mr-1.5" />
Restart
</Button>
<Button
size="sm"
variant="outline"
onClick={handleStop}
disabled={actionLoading}
>
<Square className="size-3.5 mr-1.5" />
Stop
</Button>
</>
)}
{isTransitioning && (
<Button size="sm" variant="outline" disabled>
<Activity className="size-3.5 mr-1.5 animate-pulse" />
{DAEMON_STATE_LABELS[status.state]}
</Button>
)}
</div>
</div>
</div>
<DaemonPanel open={panelOpen} onOpenChange={setPanelOpen} status={status} />
</>
);
}

View File

@@ -0,0 +1,103 @@
import { useState, useEffect, useCallback } from "react";
import { Button } from "@multica/ui/components/ui/button";
import { Switch } from "@multica/ui/components/ui/switch";
import type { DaemonPrefs } from "../../../shared/daemon-types";
function SettingRow({
label,
description,
children,
}: {
label: string;
description: string;
children: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between gap-6 py-4">
<div className="min-w-0">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
export function DaemonSettingsTab() {
const [prefs, setPrefs] = useState<DaemonPrefs>({ autoStart: true, autoStop: false });
const [cliInstalled, setCliInstalled] = useState<boolean | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
window.daemonAPI.getPrefs().then(setPrefs);
window.daemonAPI.isCliInstalled().then(setCliInstalled);
}, []);
const updatePref = useCallback(
async (key: keyof DaemonPrefs, value: boolean) => {
setSaving(true);
const updated = await window.daemonAPI.setPrefs({ [key]: value });
setPrefs(updated);
setSaving(false);
},
[],
);
return (
<div>
<h2 className="text-lg font-semibold">Daemon</h2>
<p className="text-sm text-muted-foreground mt-1">
Configure how the local agent daemon behaves with the desktop app.
</p>
<div className="mt-6 divide-y">
<SettingRow
label="Auto-start on launch"
description="Automatically start the daemon when the app opens and you are logged in."
>
<Switch
checked={prefs.autoStart}
onCheckedChange={(checked) => updatePref("autoStart", checked)}
disabled={saving}
/>
</SettingRow>
<SettingRow
label="Auto-stop on quit"
description="Stop the daemon when the desktop app is closed. Disable this to keep the daemon running in the background."
>
<Switch
checked={prefs.autoStop}
onCheckedChange={(checked) => updatePref("autoStop", checked)}
disabled={saving}
/>
</SettingRow>
<div className="py-4">
<p className="text-sm font-medium">CLI Status</p>
<p className="text-sm text-muted-foreground mt-1">
{cliInstalled === null
? "Checking…"
: cliInstalled
? "multica CLI is installed and available in PATH."
: "multica CLI not found. Install it to enable daemon management."}
</p>
{cliInstalled === false && (
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() =>
window.desktopAPI.openExternal(
"https://github.com/multica-ai/multica#cli-installation",
)
}
>
Installation Guide
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
import { useEffect } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@multica/ui/lib/utils";
import { useTabHistory } from "@/hooks/use-tab-history";
import { useActiveTitleSync } from "@/hooks/use-tab-sync";
import { useTabStore, resolveRouteIcon } from "@/stores/tab-store";
import {
SidebarProvider,
useSidebar,
} 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}
aria-label="Go back"
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}
aria-label="Go forward"
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>
);
}
// The main area's top bar doubles as a window drag region. When the sidebar
// is collapsed, we pad the left side so tabs don't land under the macOS
// traffic lights (which live at roughly x=16..68 and always hit-test above HTML).
function MainTopBar() {
const { state } = useSidebar();
const sidebarCollapsed = state === "collapsed";
return (
<header
className={cn("h-12 shrink-0", sidebarCollapsed && "pl-20")}
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
>
<TabBar />
</header>
);
}
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">
<MainTopBar />
{/* Content area with inset styling — relative so ChatWindow/ChatFab are constrained here */}
<div className="relative flex flex-1 min-h-0 flex-col overflow-hidden mr-2 mb-2 ml-0.5 rounded-xl shadow-sm bg-background">
<TabContent />
<ChatWindow />
<ChatFab />
</div>
</div>
</SidebarProvider>
</div>
<ModalRegistry />
<SearchCommand />
</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,124 @@
import { useCallback, useEffect, useState } from "react";
import { ArrowDownToLine, RefreshCw, X } from "lucide-react";
type UpdateState =
| { status: "idle" }
| { status: "available"; version: string }
| { status: "downloading"; percent: number }
| { status: "ready" };
export function UpdateNotification() {
const [state, setState] = useState<UpdateState>({ status: "idle" });
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const cleanups: (() => void)[] = [];
cleanups.push(
window.updater.onUpdateAvailable((info) => {
setState({ status: "available", version: info.version });
setDismissed(false);
}),
);
cleanups.push(
window.updater.onDownloadProgress((progress) => {
setState({ status: "downloading", percent: progress.percent });
}),
);
cleanups.push(
window.updater.onUpdateDownloaded(() => {
setState({ status: "ready" });
}),
);
return () => cleanups.forEach((fn) => fn());
}, []);
const handleDownload = useCallback(() => {
// Prevent double-click: immediately transition to downloading state
if (state.status !== "available") return;
setState({ status: "downloading", percent: 0 });
window.updater.downloadUpdate();
}, [state.status]);
const handleInstall = useCallback(() => {
window.updater.installUpdate();
}, []);
// Only allow dismiss when update is available (not during download or ready)
if (state.status === "idle") return null;
if (dismissed && state.status === "available") return null;
return (
<div className="fixed bottom-4 right-4 z-50 w-80 rounded-lg border border-border bg-background p-4 shadow-lg animate-in slide-in-from-bottom-2 fade-in duration-300">
<button
onClick={() => setDismissed(true)}
className="absolute top-2 right-2 rounded-md p-1 text-muted-foreground hover:text-foreground transition-colors"
>
<X className="size-3.5" />
</button>
{state.status === "available" && (
<div className="flex items-start gap-3">
<div className="mt-0.5 rounded-md bg-primary/10 p-1.5">
<ArrowDownToLine className="size-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">New version available</p>
<p className="text-xs text-muted-foreground mt-0.5">
v{state.version} is ready to download
</p>
<button
onClick={handleDownload}
className="mt-2 inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Download update
</button>
</div>
</div>
)}
{state.status === "downloading" && (
<div className="flex items-start gap-3">
<div className="mt-0.5 rounded-md bg-primary/10 p-1.5">
<ArrowDownToLine className="size-4 text-primary animate-pulse" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">Downloading update...</p>
<div className="mt-2 h-1.5 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{ width: `${Math.round(state.percent)}%` }}
/>
</div>
<p className="text-xs text-muted-foreground mt-1">
{Math.round(state.percent)}%
</p>
</div>
</div>
)}
{state.status === "ready" && (
<div className="flex items-start gap-3">
<div className="mt-0.5 rounded-md bg-success/10 p-1.5">
<RefreshCw className="size-4 text-success" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">Update ready</p>
<p className="text-xs text-muted-foreground mt-0.5">
Restart to apply the update
</p>
<button
onClick={handleInstall}
className="mt-2 inline-flex items-center rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Restart now
</button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,40 @@
@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 *));
/* Font stack: Inter for Latin UI text + system Chinese fonts for zh content.
Web app uses the same stack via next/font/google in apps/web/app/layout.tsx —
keep the CJK fallback tail in sync across both files. The Inter primary family
differs by design: next/font produces `__Inter_xxx` (with a synthetic size-adjusted
fallback face to prevent FOUT layout shift); desktop uses fontsource's "Inter Variable".
Both resolve to Inter glyphs, so rendering is identical in practice.
Currently covers English + Simplified Chinese. When ja/ko i18n lands, extend
the tail with Hiragino Kaku Gothic ProN / Yu Gothic / Apple SD Gothic Neo / Malgun Gothic.
Per-character fallback: Latin chars render with Inter, Chinese chars with
PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans CJK SC (Linux).
Mono font has no explicit CJK fallback: CJK chars in code blocks are inherently
non-aligned with a mono grid (Chinese is proportional), so listing CJK fonts
would falsely signal alignment guarantees. Browser default fallback handles
the rare mixed case correctly. */
:root {
--font-sans: "Inter Variable", "Inter", -apple-system, BlinkMacSystemFont,
"Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC",
sans-serif;
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
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";
// Inter variable font covers all weights (100-900) in a single file.
// Geist Mono kept as-is for code blocks; CJK is handled by system font fallback
// (see globals.css --font-sans chain). Keep font stack in sync with apps/web/app/layout.tsx.
import "@fontsource-variable/inter";
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 { AutopilotDetailPage as AutopilotDetail } from "@multica/views/autopilots/components";
import { useWorkspaceId } from "@multica/core/hooks";
import { autopilotDetailOptions } from "@multica/core/autopilots/queries";
import { useDocumentTitle } from "@/hooks/use-document-title";
export function AutopilotDetailPage() {
const { id } = useParams<{ id: string }>();
const wsId = useWorkspaceId();
const { data } = useQuery(autopilotDetailOptions(wsId, id!));
useDocumentTitle(data ? `${data.autopilot.title}` : "Autopilot");
if (!id) return null;
return <AutopilotDetail autopilotId={id} />;
}

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,34 @@
import { LoginPage } from "@multica/views/auth";
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
const WEB_URL = import.meta.env.VITE_WEB_URL || "http://localhost:3000";
export function DesktopLoginPage() {
const lastWorkspaceId = localStorage.getItem("multica_workspace_id");
const handleGoogleLogin = () => {
// Open web login page in the default browser with platform=desktop flag.
// The web callback will redirect back via multica:// deep link with the token.
window.desktopAPI.openExternal(
`${WEB_URL}/login?platform=desktop`,
);
};
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" />}
lastWorkspaceId={lastWorkspaceId}
onSuccess={() => {
// Auth store update triggers AppContent re-render → shows DesktopShell
}}
onGoogleLogin={handleGoogleLogin}
/>
</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,121 @@
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";
// Public web app URL — injected at build time via .env.production. Falls
// back to the production host for dev builds so "Copy link" yields a URL
// that actually points somewhere a teammate can open.
const APP_URL = import.meta.env.VITE_APP_URL || "https://multica.ai";
/**
* 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) => `${APP_URL}${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) => `${APP_URL}${path}`,
}),
[router, location],
);
return <NavigationProvider value={adapter}>{children}</NavigationProvider>;
}

View File

@@ -0,0 +1,150 @@
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 { AutopilotDetailPage } from "./pages/autopilot-detail-page";
import { IssuesPage } from "@multica/views/issues/components";
import { ProjectsPage } from "@multica/views/projects/components";
import { AutopilotsPage } from "@multica/views/autopilots/components";
import { MyIssuesPage } from "@multica/views/my-issues";
import { RuntimesPage } from "@multica/views/runtimes";
import { SkillsPage } from "@multica/views/skills";
import { DaemonRuntimeCard } from "./components/daemon-runtime-card";
import { AgentsPage } from "@multica/views/agents";
import { InboxPage } from "@multica/views/inbox";
import { SettingsPage } from "@multica/views/settings";
import { OnboardingWizard } from "@multica/views/onboarding";
import { InvitePage } from "@multica/views/invite";
import { useNavigation } from "@multica/views/navigation";
import { Server } from "lucide-react";
import { DaemonSettingsTab } from "./components/daemon-settings-tab";
/**
* 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 />
</>
);
}
function OnboardingRoute() {
const nav = useNavigation();
return <OnboardingWizard onComplete={() => nav.push("/issues")} />;
}
function InviteRoute() {
const matches = useMatches();
const match = matches.find((m) => (m.params as { id?: string }).id);
const id = (match?.params as { id?: string })?.id ?? "";
return <InvitePage invitationId={id} />;
}
/** 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: "autopilots",
element: <AutopilotsPage />,
handle: { title: "Autopilot" },
},
{
path: "autopilots/:id",
element: <AutopilotDetailPage />,
handle: { title: "Autopilot" },
},
{
path: "my-issues",
element: <MyIssuesPage />,
handle: { title: "My Issues" },
},
{
path: "runtimes",
element: <RuntimesPage topSlot={<DaemonRuntimeCard />} />,
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: "onboarding",
element: <OnboardingRoute />,
handle: { title: "Get Started" },
},
{
path: "invite/:id",
element: <InviteRoute />,
handle: { title: "Accept Invite" },
},
{
path: "settings",
element: (
<SettingsPage
extraAccountTabs={[
{
value: "daemon",
label: "Daemon",
icon: Server,
content: <DaemonSettingsTab />,
},
]}
/>
),
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,196 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { arrayMove } from "@dnd-kit/sortable";
import { createPersistStorage, defaultStorage } from "@multica/core/platform";
import { createSafeId } from "@multica/core/utils";
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 createSafeId();
}
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,53 @@
export type DaemonState =
| "running"
| "stopped"
| "starting"
| "stopping"
| "installing_cli"
| "cli_not_found";
export interface DaemonStatus {
state: DaemonState;
pid?: number;
uptime?: string;
daemonId?: string;
deviceName?: string;
agents?: string[];
workspaceCount?: number;
/** CLI profile this daemon belongs to. Empty string means the default profile. */
profile?: string;
/** Backend URL the daemon connects to. */
serverUrl?: string;
}
export interface DaemonPrefs {
autoStart: boolean;
autoStop: boolean;
}
export const DAEMON_STATE_COLORS: Record<DaemonState, string> = {
running: "bg-emerald-500",
stopped: "bg-muted-foreground/40",
starting: "bg-amber-500 animate-pulse",
stopping: "bg-amber-500 animate-pulse",
installing_cli: "bg-sky-500 animate-pulse",
cli_not_found: "bg-red-500",
};
export const DAEMON_STATE_LABELS: Record<DaemonState, string> = {
running: "Running",
stopped: "Stopped",
starting: "Starting…",
stopping: "Stopping…",
installing_cli: "Setting up…",
cli_not_found: "Setup Failed",
};
export function formatUptime(uptime?: string): string {
if (!uptime) return "";
const match = uptime.match(/(?:(\d+)h)?(\d+)m/);
if (!match) return uptime;
const h = match[1] ? `${match[1]}h ` : "";
const m = match[2] ? `${match[2]}m` : "";
return `${h}${m}`.trim() || uptime;
}

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/*"
]
}
}
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
include: ["src/**/*.test.ts", "scripts/**/*.test.mjs"],
environment: "node",
passWithNoTests: true,
},
});

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,96 @@
---
title: CLI Installation
description: Install the Multica CLI and start the agent daemon.
---
## Installation
### Homebrew (macOS/Linux)
```bash
brew install multica-ai/tap/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
brew upgrade multica-ai/tap/multica
```
For install script or manual installs, use:
```bash
multica update
```
`multica update` auto-detects your installation method and upgrades accordingly.
## Quick Start
```bash
# One command: configure, authenticate, and start the daemon
multica setup
```
This configures the CLI for Multica Cloud, opens your browser for login, discovers your workspaces, and starts the agent daemon.
For self-hosted servers, use `multica setup self-host` instead. See [Self-Hosting](/docs/getting-started/self-hosting) for details.
## Verify
```bash
multica daemon status
```
Confirm:
1. Status is `running`
2. At least one agent is listed (e.g. `claude`, `codex`, `gemini`, `opencode`, `openclaw`, or `hermes`)
3. At least one workspace is being watched
If the agents list is empty, install at least one supported AI agent CLI:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude`)
- [Codex](https://github.com/openai/codex) (`codex`)
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) (`gemini`)
- OpenCode (`opencode`)
- OpenClaw (`openclaw`)
- Hermes (`hermes`)
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,320 @@
---
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 |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | Google's coding agent |
| OpenCode | `opencode` | Open-source coding agent |
| OpenClaw | `openclaw` | Open-source coding agent |
| Hermes | `hermes` | Nous Research 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 |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
| `MULTICA_OPENCLAW_MODEL` | Override the OpenClaw model used |
| `MULTICA_HERMES_PATH` | Custom path to the `hermes` binary |
| `MULTICA_HERMES_MODEL` | Override the Hermes model used |
| `MULTICA_GEMINI_PATH` | Custom path to the `gemini` binary |
| `MULTICA_GEMINI_MODEL` | Override the Gemini 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
# Set up a staging profile
multica setup self-host --profile staging --server-url https://api-staging.example.com --app-url https://staging.example.com
# Start its daemon
multica daemon start --profile staging
# 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,64 @@
---
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, Gemini CLI, 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:
### macOS / Linux (Homebrew - recommended)
```bash
brew install multica-ai/tap/multica
```
### macOS / Linux (install script)
```bash
# Install the CLI
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
```
### Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex
```
Then configure, authenticate, and start the daemon:
```bash
# Configure, authenticate, and start the daemon
multica setup
```
The daemon auto-detects available agent CLIs (`claude`, `codex`, `gemini`, `openclaw`, `opencode`, `hermes`) 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, Gemini CLI, OpenClaw, OpenCode, or Hermes). 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,396 @@
---
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
Two commands to set up everything:
```bash
# Install CLI + provision self-host server
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server
# Configure CLI, authenticate, and start the daemon
multica setup self-host
```
This clones the repo, starts all services, installs the CLI, and configures it for localhost. Then open http://localhost:3000 — log in with any email + code **`888888`**.
<Callout>
If the self-host server is already running and you only need the CLI on a macOS/Linux machine, install it with Homebrew: `brew install multica-ai/tap/multica`.
</Callout>
<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)
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) (`gemini` on PATH)
- OpenCode (`opencode` on PATH)
- OpenClaw (`openclaw` on PATH)
- Hermes (`hermes` on PATH)
### b) One-command setup
```bash
multica setup self-host
```
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
For on-premise deployments with custom domains:
```bash
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
```
Verify the daemon is running:
```bash
multica daemon status
```
<Callout>
Alternatively, configure step by step: `multica config set server_url http://localhost:8080 && multica config set app_url http://localhost:3000 && 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 setup
```
This reconfigures the CLI for multica.ai, re-authenticates, and restarts the daemon. You will be prompted before overwriting the existing configuration.
<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 |
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 |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
| `MULTICA_OPENCLAW_MODEL` | Override the OpenClaw model used |
| `MULTICA_HERMES_PATH` | Custom path to the `hermes` binary |
| `MULTICA_HERMES_MODEL` | Override the Hermes model used |
| `MULTICA_GEMINI_PATH` | Custom path to the `gemini` binary |
| `MULTICA_GEMINI_MODEL` | Override the Gemini model used |
## 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,57 @@
---
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, Gemini CLI, OpenClaw, OpenCode, or Hermes)
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 |
| Gemini CLI | `gemini` | Google's coding agent |
| OpenClaw | `openclaw` | Open-source coding agent |
| OpenCode | `opencode` | Open-source coding agent |
| Hermes | `hermes` | Nous Research coding agent |
The daemon auto-detects which CLIs are available on your PATH and registers them as available runtimes.
## Reusable Skills
Multica supports two layers of skills:
- **Local skills** — Skills already installed in your local runtime (e.g., `.claude/skills/`, `.config/opencode/skills/`) are automatically discovered and used by agents. You do **not** need to upload them to Multica.
- **Workspace skills** — Skills created or imported in the Multica Skills page are shared across the workspace. They are automatically injected into agent runs as supplementary context, so every team member's agents benefit from them.
Workspace skills are designed for team-wide sharing and collaboration — codify your team's best practices once, and every agent can leverage them:
- Deployments
- Migrations
- Code reviews
- Common patterns
Your skill library compounds over time. Local skills give individual agents their capabilities; workspace skills align the entire team.
## 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,30 @@
---
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. Set up and start the daemon
```bash
multica setup # Configure, authenticate, and start the daemon
```
This configures the CLI, opens your browser for login, discovers your workspaces, and starts the agent daemon in the background. It auto-detects agent CLIs (`claude`, `codex`, `gemini`, `openclaw`, `opencode`, `hermes`) 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, Gemini CLI, OpenClaw, OpenCode, or Hermes). 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**, **Gemini CLI**, **OpenClaw**, **OpenCode**, and **Hermes**.
## 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, Gemini CLI, OpenClaw, OpenCode, or Hermes |
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Next.js │────>│ Go Backend │────>│ PostgreSQL │
│ Frontend │<────│ (Chi + WS) │<────│ (pgvector) │
└──────────────┘ └──────┬───────┘ └──────────────────┘
┌──────┴───────┐
│ Agent Daemon │ (runs on your machine)
│Claude/Codex/ │
│Gemini/Hermes │
└──────────────┘
```
## 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;

Some files were not shown because too many files have changed in this diff Show More