Commit Graph

512 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Lyon Liang
418049856f merge: resolve conflicts with upstream/main 2026-04-14 12:49:40 +08: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
Lyon Liang
9170b01739 fix(views): focus description editor when clicking empty area 2026-04-14 11:04:58 +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
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
Jiayuan Zhang
5b4ee7c5e1 fix(workspace): surface slug conflicts (#895) 2026-04-14 00:09:12 +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
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
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
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
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
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
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
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
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
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