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