* 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>
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>
- 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>
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>
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>
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>
* 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.
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>
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>
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.
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.
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>
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.
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>
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.
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
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>
* 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.
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.
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
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>.
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
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>
- 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>
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>
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
* 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
- 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>
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>
- 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>
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
* 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.