* feat(issues): show project segment in issue breadcrumb (MUL-2422)
Render the issue's project (when present) between the workspace and any
parent-issue segment. Segment reflects the issue's own `project_id` so
the same URL produces the same breadcrumb from every entry point.
Failed/missing project queries fall back to an "Unknown project"
placeholder; loading shows a skeleton to avoid layout shift.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): cap project breadcrumb width to preserve title precedence
Constrain Project crumb to max-w-72 (matching ProjectChip) and add
min-w-0 to the title span so the flex compression order matches RFC
§5/§9: Project/Parent shrink before the current Issue title.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(runtimes): declutter the runtimes page (MUL-2407)
Cuts visual noise on the Runtimes detail view without removing real
information:
- MachineDetail: drop the 4-card metric grid (RUNTIMES / HEALTH /
WORKLOAD / CLI) and replace it with a single inline meta strip. The
cards repeated what the title chip and runtime rows already show.
- PageHeaderBar: remove the inline tagline + "Learn more" link. The
header is now icon + title + count + connect button.
- VisibilityBadge: only render the Public chip. Private is the default,
so a row of `🔒 Private` badges was pure noise.
- CliCell: drop the per-row "Desktop" managed badge — the same string on
every desktop row carried near-zero information.
- MachineSidebar row: hide the truncated daemon-id subtitle. The id is
still available on hover via `title` and remains visible in the
detail header.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): address review feedback on inline meta and hover title
- Inline meta now reads "6 runtimes · 5 online" instead of "6 6 online"
by using runtime_count for the total label.
- Sidebar machine title hover now shows full daemon id (with subtitle
fallback) so the daemon id is recoverable after the sub-row was hidden.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
320px was too cramped for typical rendered HTML (charts, dashboards,
formatted documents). Matches the existing HTML attachment preview
height for visual consistency across both iframe surfaces.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438)
Drops the post-onboarding ImportStarterContent / DismissStarterContent
flow (handler + routes + StarterContentPrompt + templates + locale
strings + analytics event). The bug — web onboarding seeding 6+ starter
issues without a runtime — only existed through that path; with it gone
the source disappears.
The "install a runtime" issue from BootstrapOnboardingNoRuntime is now
the canonical no-runtime onboarding seed. The title/description and a
LockAndFindActiveDuplicate-deduped seeder move to
handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace /
AcceptInvitation seed it whenever the workspace has no runtime yet, so
every mark-onboarded entry point lands the user on a concrete next
step.
starter_content_state column is kept and continues to be claimed as
'imported' in all five entry points so older desktop builds (which
still render the legacy dialog on NULL) don't surface it to accounts
created after this change.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438)
054 only covered pre-feature users. Anyone onboarded between then and the
starter-content kit removal could still sit at NULL, and old desktop
clients gate the legacy StarterContentPrompt on `starter_content_state
IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL
would surface a dialog whose buttons 404. Mark them 'imported' to match
the new helper's claim semantics.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Add optional `opts.activate` to NavigationAdapter.openInNewTab. Default
stays `false` so cmd/ctrl+click on links/mentions keeps browser-style
background semantics. The two explicit toolbar entry points
(attachment-preview-modal, html-attachment-preview) opt in with
`{ activate: true }` so the new tab gains focus after the modal closes.
Both desktop providers (root + per-tab) now use the tab id returned by
`store.openTab` to call `setActiveTab` only when `activate` is true.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
- Drop `workspace current`; `workspace get` (no args) already prints the
current default workspace, so the two were doing the same thing.
- Rename `workspace members` to `workspace member list` to free up the
`member` namespace for future `add` / `remove` subcommands and align
with the rest of the CLI's `<resource> <verb>` shape.
- Add `--full-id` to `workspace list`, matching `project list`,
`autopilot list`, and friends.
Docs and the daemon prompt are updated to match.
Co-authored-by: multica-agent <github@multica.ai>
* fix(openclaw): parse whole buffer instead of line-by-line scanner
Follow-up to c87d7676 (WOR-10). The stdout/stderr swap fixed the dominant
case but `processOutput` still scanned line-by-line and only attempted a
whole-buffer parse from a fragile fallback path. Pretty-printed JSON
(openclaw 2026.5.x emits the result blob indented across many lines) made
every individual line unparseable on its own — `{`, ` "payloads": [`,
` {`, etc. — so the success path hinged entirely on the fallback
joining `rawLines` and re-trying.
Under load (daemon restarts racing the close-on-cancel goroutine, partial
chunked reads when stdout closes mid-flight) the line scanner could see
truncated input that never reassembled into valid JSON, surfacing
"openclaw returned no parseable output" against runs where the agent had
in fact completed the work and posted comments. Roughly 30–40% of recent
runs in v0.2.27 logs hit this path; multica still wrote a `task_failed`
inbox row for each one even though the underlying issue had moved to
`in_review` or `done`.
The fix:
- processOutput now reads the full stdout buffer with `io.ReadAll` first.
- A new `parseWholeBufferOpenclawResult` helper attempts a single
`json.Unmarshal` against the entire buffer (after trimming, and after
optionally stripping leading non-JSON log lines). When it matches, we
build the result and return — the line scanner never runs.
- If the whole-buffer parse fails, we fall through to the existing NDJSON
line-by-line scanner. This preserves streaming-event support (kept for
forward compatibility and other backends) without leaving openclaw's
dominant pretty-printed shape at the mercy of timing.
- The failure path now emits a `(got N bytes; preview: ...)` suffix on
the canonical "no parseable output" error so future debugging isn't
blind. The exact canonical phrase is preserved for empty buffers so
existing dashboards / log-grep tooling keep matching.
Tests:
- TestOpenclawProcessOutputWholeBufferPrettyJSON: feeds a hand-crafted
multi-line indented blob (multiple payloads, nested agentMeta, usage
map) and asserts every field round-trips through the whole-buffer fast
path.
- TestOpenclawProcessOutputDeeplyIndentedFixture: re-runs the recorded
openclaw 2026.5.5 stdout fixture (1070 lines) directly through
parseWholeBufferOpenclawResult, asserting the bug-shape parses cleanly
on the first attempt without falling through to NDJSON scanning.
- TestOpenclawProcessOutputEmptyBufferErrorIncludesByteCount: tightens
the empty-buffer failure path, asserts the canonical phrase survives so
observability tooling keeps working.
All existing tests in the openclaw + buildOpenclawArgs suites stay green
(streaming NDJSON event tests, lifecycle tests, structured-error tests,
usage-field-variant tests). The two pre-existing flaky timeout-tight
codex tests (TestCodexExecuteSemanticInactivityAllowsContinuous*) fail on
both this branch and on c87d7676 baseline; they are unrelated and out of
scope here.
Co-authored-by: multica-agent <github@multica.ai>
* fix(openclaw): drop dead preview branch, document streaming regression
Rebase + review-fix follow-up on top of f27df2d9b.
processOutput's preview branch was unreachable: openclawNoParseableOutputError
was only called from the `!gotEvents && trimmed == ""` path, which by
construction means the entire scanned buffer collapsed to whitespace, so the
`(got N bytes; preview: ...)` formatter could never fire on a non-empty buffer.
Replace the helper with a single canonical-string constant (callsite is now
inline) and update the test name to match what it actually asserts (the
canonical empty-buffer error string is preserved for external log-grep /
dashboard consumers).
Also document on processOutput that the line-scanner path is no longer
truly streaming after the io.ReadAll switch: events accumulate until
stdout closes. OpenClaw 2026.5.x does not emit streaming events so this
regression is invisible today, but flag it for the next backend that
might.
Misc: switch the scanner's input source from
`strings.NewReader(string(buf))` to `bytes.NewReader(buf)` to drop one
unnecessary byte/string round-trip.
MUL-1908
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J (Multica agent) <j@multica.local>
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881)
Project Gantt now fetches its own scheduled-only data instead of riding the
Board/List pagination cache. The Unscheduled drawer and pagination warning
banner are gone, and any WS-driven issue change (create / update / delete)
invalidates the new cache so the timeline stays live.
- Backend: `GET /api/issues?scheduled=true` adds an
`(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both
ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler.
- Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single
fetch and lives under its own cache key. WS handlers and mutations
invalidate the prefix on create/update/delete so the bar reacts to
start_date / due_date changes from other tabs and from this tab without
waiting on the WS round-trip.
- GanttView: drops the Unscheduled section, the pagination warning banner,
and the load-all button; renders only scheduled rows.
- Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`,
`summarizeIssueListPagination`, and the gantt locale strings that
supported the old plumbing.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): page through Gantt fetch and isolate per-view data sources
- Walk paginated `scheduled=true` issues until total is reached so projects
with more than 500 scheduled bars no longer silently truncate.
- Gantt mode disables the bucketed Board/List query and reads its own
scheduled cache for the project empty-state check, so the page never
short-circuits Gantt with a Board-derived "no issues" CTA.
- `onIssueLabelsChanged` patches matching rows in the Project Gantt cache
in-place, keeping label filters consistent after attach/detach from
other tabs or agents.
MUL-1881
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Earlier the unification commit dragged in a Tailwind override stack
(ring, rounded-md, transition-shadow, bg-background/95, button hover
classes) "to make standalone surfaces work without .rich-text-editor
scope". Because the legacy CSS rules were not removed, both layers
applied in the editor, producing a visible double-stroke selection
ring and a light-theme hover on top of the dark-glass toolbar.
This commit reverts the styling churn:
- ImageAttachmentView now emits the same span-only DOM as the original
ReadonlyImage: <span.image-node> > <span.image-figure> > <img.image-content>
+ <span.image-toolbar> with naked <button> children. No Tailwind tax.
- The `.image-*` rules in content-editor.css are de-scoped from
`.rich-text-editor` so the single set of styles also drives chat /
AttachmentList renders. Editor-only behavior (640px cap, NodeView
centering) stays under the `.rich-text-editor` scope.
- A `data-clickable` attribute carries the "this image is clickable
to preview" hint that the readonly cursor rule used to key off the
`.rich-text-editor.readonly` scope.
- ImageView NodeViewWrapper no longer adds its own `image-node` class
because `<Attachment>` already emits one; the duplicate was harmless
but redundant.
Visual: editor + readonly comments render identical to before. Chat /
AttachmentList previously rendered a gray file card for images (the
P0 fix in the parent commit) and now match the editor visual without
the heavy-handed Tailwind detour.
Tests: 98 attachment-related tests pass; full `pnpm typecheck` + `pnpm
test` (652 tests) green.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent root causes made "Open in new tab" on a desktop
attachment-preview modal feel like "the popup is still there and the
current tab got replaced":
1. `AttachmentPreviewModal.handleOpenInNewTab` never called `onClose()`,
so the modal stayed mounted over the new tab.
2. Both `DesktopNavigationProvider.openInNewTab` and
`TabNavigationProvider.openInNewTab` called
`store.setActiveTab(tabId)` after `store.openTab(...)`, which stole
focus to the new tab — violating the type contract
("Desktop only: open a path in a new background tab") and matching
neither Chrome's cmd+click default nor the user's expectation.
Fixes:
- Modal: always call `onClose()` after dispatching the navigation
(desktop adapter path and web `window.open` fallback path).
- Desktop navigation: drop the post-`openTab` `setActiveTab` call in both
providers. `openTab` already preserves `activeTabId` for new paths and
switches to the existing tab when the path is already open, which is
exactly the background-tab semantics the type contract advertises.
Tests:
- `attachment-preview-modal.test.tsx`: assert `onClose` is invoked on
both the desktop and web fallback branches.
- `pageview-tracker.test.tsx`: rename the "openInNewTab / addTab" case
so the comment no longer claims `openInNewTab` activates the new tab.
- New `apps/desktop/.../platform/navigation.test.tsx`: assert that
`openInNewTab` on both providers calls `openTab` and never
`setActiveTab` for same-workspace paths, and routes cross-workspace
paths through `switchWorkspace`.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
HTML attachment previews mount the document inside a sandboxed
`<iframe srcdoc>` deliberately WITHOUT `allow-same-origin` — uploads are
untrusted user content. Chromium treats fragment-link clicks inside such an
opaque-origin srcdoc iframe as cross-origin frame navigation and silently
rejects them, so clicking a TOC entry never scrolls.
Append a tiny shim script to the srcdoc that intercepts `<a href="#...">`
clicks inside the iframe and calls `scrollIntoView` directly. The shim runs
in the iframe's own opaque origin under `allow-scripts` — no new
capabilities, no sandbox token changes; it cannot reach parent / cookies /
localStorage.
All three HTML attachment surfaces share the same helper:
- inline 480px card (html-attachment-preview.tsx)
- full-screen modal (attachment-preview-modal.tsx)
- full-page route (attachment-preview-page.tsx)
References: whatwg/html#3537, crbug 40191760.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
When the renderer crashes hard enough to leave a white window (React
boundary unrecoverable, syntax error during initial mount, preload
script throw), DevTools can't be opened and the only signal in the
`make dev` terminal is the daemon-manager 5s polling complaint
("Render frame was disposed before WebFrameMain could be accessed").
That's a downstream symptom — the actual JS error is unreachable, so
the user has no path to diagnose without restarting the renderer
(which loses the failure mode entirely).
Add four webContents listeners on the main BrowserWindow, gated by
`is.dev` so packaged builds keep their stderr clean:
- `console-message`: forwards every renderer `console.*` to main's
stderr with file:line. React error boundaries, `window.onerror`, and
unhandled-rejection handlers all surface here.
- `render-process-gone`: serialises the GoneDetails (`crashed` / `oom`
/ `killed` / `launch-failed`) so the user sees *why* the renderer
died, not just that it did.
- `did-fail-load`: catches loadURL/loadFile failures. Skip
`errorCode === -3 (ABORTED)` because that's the normal HMR-induced
navigation abort.
- `preload-error`: the one error class DevTools can never show, because
preload runs before the window owns a console. Without this listener
preload throws are invisible.
All output is prefixed with `[renderer <tag>]` so it's easy to grep
distinct from main's own logs.
No behavioural change in production: the entire block is inside an
`is.dev` guard. Packaged builds keep their existing stderr.
Collapse the five separate attachment render paths (file-card NodeView,
image NodeView, readonly markdown img/fileCard renderers, AttachmentList
standalone fallback, and the parallel packages/ui/markdown renderer) into
one <Attachment attachment={a} /> dispatcher.
Fixes a P0 visual regression: a PNG attached to a message but not inlined
in the markdown body used to render as a gray "file card" because
getPreviewKind() lacked an "image" branch and image rendering bypassed
the dispatcher entirely. Now every surface routes through <Attachment>,
so the same PNG renders as a real <img> with hover toolbar and
preview-modal everywhere.
Key changes:
- PreviewKind gains "image"; getPreviewKind() detects image/* + common
extensions before the html/text branches (so svg stays image, not text).
- AttachmentPreviewModal gains case "image" (replaces the standalone
ImageLightbox, which is deleted).
- New packages/views/editor/attachment.tsx owns all kind-aware routing
(image | html | file) and dispatches preview modal + download via the
existing useAttachmentPreview / useDownloadAttachment hooks. Subsumes
the deleted AttachmentBlock.
- AttachmentInput.url accepts a forceKind hint so callers that *know*
the structural kind (markdown , Tiptap image node) skip the
filename-based autodetect — fixes a regression where empty or
descriptive alt text would route an image to the file-card chrome.
- Tiptap NodeViews (file-card.tsx, image-view.tsx) shrink to thin
wrappers that forward editor hints (selected, deleteNode, uploading)
to <Attachment>.
- ReadonlyContent and AttachmentList each mount their own
AttachmentDownloadProvider so url → record resolution works outside
ContentEditor's provider.
- packages/ui/markdown gains optional renderImage / renderFileCard slot
props; packages/views/common/markdown.tsx injects <Attachment> into
those slots and threads message attachments through to chat /
skill-file viewers.
- chat-message-list passes message.attachments to every <Markdown> call
site and renders a standalone AttachmentList under each bubble for
attachments not referenced in the body.
Tests: attachment.test.tsx covers 9 scenarios (record image / pdf / html;
url-only image with resolver hit and miss; uploading state; editable
delete; forceKind regression). attachment-preview-modal.test.tsx gains
image-dispatch cases. 652/652 unit tests pass.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(auth): make auth token TTL configurable via AUTH_TOKEN_TTL env var
Add AUTH_TOKEN_TTL environment variable (in seconds) to override the
hardcoded 30-day auth token lifetime. Self-hosted deployments on trusted
networks can set a longer value to avoid frequent magic-link
re-authentication.
The value is read once at startup and cached. Invalid or missing values
fall back to the 30-day default with a warning log.
Closes#2685
* refactor(auth): extract parseAuthTokenTTL for testability
Address review feedback: extract pure parse function from sync.Once
wrapper so the parsing logic can be unit-tested independently.
Add TestParseAuthTokenTTL with table-driven cases.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* refactor(auth): accept Go duration strings + hoist shared TTL in SetAuthCookies
Address nice-to-have review feedback from Bohan-J:
- parseAuthTokenTTL now tries time.ParseDuration first (e.g. '8760h'),
falling back to ParseInt for integer seconds
- Warn on unreasonable values (>10 years) but still accept them
- Hoist AuthTokenTTL() and time.Now() in SetAuthCookies so both
cookies share the exact same expiry
- Add security trade-off note in .env.example
- Add 5 new test cases for duration strings
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com>
* fix: use AuthTokenTTL() in CloudFront middleware, guard ParseInt overflow
Address review feedback from Bohan-J (round 2):
1. CloudFront refresh middleware (cloudfront.go:21) was hardcoding
30*24*time.Hour instead of using auth.AuthTokenTTL(). Now calls
AuthTokenTTL() so the middleware respects AUTH_TOKEN_TTL env var.
2. parseAuthTokenTTL integer-seconds branch: very large values like
9999999999 would silently overflow int64 when multiplied by
time.Second. Added overflow guard comparing against
math.MaxInt64/int64(time.Second) before the multiplication.
3. Updated AuthTokenTTL() doc comment to reflect that it accepts
Go duration strings or integer seconds (not just seconds).
4. Added middleware test (cloudfront_test.go) verifying short
AUTH_TOKEN_TTL produces short cookie expiry, not 30-day hardcode.
Also covers nil signer and existing-cookie-skip cases.
5. Added integer overflow test case to cookie_test.go.
* style: run gofmt on cookie.go and cookie_test.go
---------
Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com>
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(runtimes): anchor OpenCode skill + AGENTS.md discovery to task workdir
OpenCode resolves its project discovery root from `--dir` and `PWD`
before falling back to `process.cwd()`. The daemon set `cmd.Dir =
workDir` but never overrode the inherited `PWD`, so OpenCode walked
from the daemon's shell directory and silently bypassed the per-task
workdir — agents lost visibility into `.opencode/skills/` and
`AGENTS.md`, falling back to whatever global skills the host had
installed (MUL-2416).
- Pass `opencode run --dir <workDir>` and override `PWD=<workDir>` in
the child env so AGENTS.md walk-up + `.opencode/skills` project
config scan both anchor on the task workdir.
- Block `--dir` from custom args so user overrides cannot re-introduce
the regression.
- Plumb skill `description` from DB through service / daemon /
execenv. `writeSkillFiles` synthesizes a YAML frontmatter block
(`name`, optional `description`) when the stored content lacks one,
since runtimes like OpenCode silently drop SKILL.md files without a
parseable `name`. Existing frontmatter is preserved unchanged so
upstream-imported skills (GitHub / ClawHub / Skills.sh) keep their
hand-shaped metadata.
Tests:
- New fake-CLI test confirms argv carries `--dir <workDir>` and the
child sees `PWD=<workDir>`.
- New test confirms a user-supplied `--dir` in custom_args is dropped.
- New execenv tests cover synthesized frontmatter and preservation of
pre-existing frontmatter.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): inject SKILL.md `name` when upstream frontmatter omits it
Skills imported with frontmatter that sets `description` but leaves `name`
implicit (relying on the directory slug, as common in GitHub/Skills.sh
imports) still hit OpenCode's "no parseable name → drop" path because the
DB Name fallback never made it into the SKILL.md body. ensureSkillFrontmatter
now scans the existing block and, when name is missing or empty, prepends
`name: <slug>` while preserving description, body, and any runtime-specific
keys verbatim.
Also tighten yamlEscapeInline to always double-quote so descriptions that
look like YAML keywords (`null`, `true`, `[foo]`, `{x: y}`, `2024-01-01`)
parse as strings rather than getting reinterpreted and rejected.
Adds regression test for the nameless-frontmatter case and updates the
existing OpenCode skill test for the always-quoted description format.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Adds a header toggle that lets users flip the agent transcript between
chronological (oldest first, current behavior) and newest-first. The
preference is persisted via a small Zustand store. Default stays
chronological so existing readers see no behavior change.
Sort is a pure presentation concern — the underlying timeline (seq
numbers, filter keys, segment navigation) is untouched. Toggling resets
the scroll container to the top so the user lands on the newest end of
the chosen direction. Copy-all respects the displayed order so the
exported text matches what's on screen.
Scope is limited to the task transcript dialog per the MVP plan; the
issue execution log and agent activity tab are out of scope and may be
revisited once this interaction validates.
Closes GH #2736.
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): add Project Gantt view (MUL-1881)
Adds Gantt as a third option in the Project page's view toggle (Board /
List / Gantt). Bars span start_date → due_date; issues with only one
date render as markers, issues with neither are collapsed into an
Unscheduled section. Toolbar exposes day/week/month zoom and a
show-completed toggle. The Gantt view shares the existing IssuesHeader
filters/sort.
Implementation is self-rendered SVG/HTML — no new dependencies. UTC
day-aligned date math keeps bars on the right columns regardless of
viewer timezone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): scope Gantt to project surface + warn on hidden pages
- IssuesHeader / IssueDisplayControls now take `allowGantt` (default false);
only Project Detail opts in. /issues, /my-issues and the actor panel no
longer expose a Gantt option that silently fell through to List, and the
toggle icon falls back to List when a stored `viewMode === "gantt"` lands
on a surface that doesn't render it.
- Project Gantt now surfaces a banner with hidden-issue count plus a
Load-all action that drains every remaining paginated page into the
cache via the new `useLoadAllRemaining` helper. Pagination summary comes
from `myIssueListPaginationOptions`, which shares the existing cache key
with `myIssueListOptions` so totals stay in sync with Board/List.
- ScheduledRow normalizes a `start_date > due_date` anomaly to min/max and
outlines the bar with a destructive ring + tooltip note, instead of
silently dropping the row.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): simplify runtime bootstrap
* fix(onboarding): close private-helper reuse hole and guide-issue nav race
- server: when bootstrap looks for an existing Multica Helper, require
Visibility="workspace" so a private helper owned by another member
can't be auto-assigned to the onboarding issue (and trigger a task as
that private agent), which would have bypassed canAccessPrivateAgent.
- web onboarding page: refreshMe() inside bootstrap flips hasOnboarded
before onComplete fires, letting the guard's router.replace overtake
onComplete's router.push to the new guide issue. Mark the page as
"completing" right before navigating so the guard stays silent during
the in-flight transition.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): escape daemon command literals to satisfy i18next/no-literal-string
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Lambda <lambda@multica.ai>
The two `<code>` blocks in the "having trouble?" disclosure of the
Connect Remote dialog render literal shell commands ("multica daemon
status" and "multica daemon logs -f"). The `i18next/no-literal-string`
rule (enforced as error across packages/views) flagged them, turning
@multica/views#lint red on main since the dialog landed.
These strings are inherently locale-agnostic — they are the actual
commands users type into a shell, identical in every language. Wrapping
them in t() would be wrong (translators would have no source-of-truth
about whether the binary name `multica` or the subcommand `daemon` could
be translated; the answer is "never").
Mark them as exempt with `eslint-disable-next-line i18next/no-literal-string`
+ a one-line comment explaining why. Mirrors how shell-command snippets
are treated elsewhere in the repo.
Verification:
- `pnpm --filter @multica/views lint` → 0 errors (was 2). 13 remaining
warnings are pre-existing in other files and don't fail CI.
- Cascaded failures (@multica/views#typecheck, web/desktop builds) on CI
were strictly downstream of the lint failure; they'll go green once
lint passes.
* feat(settings): allow editing workspace issue prefix (MUL-2369)
Workspace admins can now change the issue prefix from Settings → General.
The change is gated by a confirmation dialog that warns about external
references (PR titles, branch names, links) breaking, because issue
identifiers are rendered as `prefix-N` on the fly — changing the prefix
effectively renames every existing issue.
Refs https://github.com/multica-ai/multica/issues/2797
Co-authored-by: multica-agent <github@multica.ai>
* fix(settings): invalidate issue cache when workspace prefix changes (MUL-2369)
Issue identifiers (`MUL-123`) are recomputed from `workspace.issue_prefix`
at read time, so cached issues kept showing the old `OLD-N` keys after a
prefix change. Without invalidation the confirm dialog's "all issues will
be renumbered" promise was broken until a hard refresh — and other tabs
receiving the `workspace:updated` WS event saw the same drift.
- WorkspaceTab: after a prefix-changing save, invalidate `issueKeys.all`
in addition to the workspace list. Non-prefix saves stay cheap.
- Realtime: split `workspace:updated` out of the generic `workspace`
refresh into a specific handler that compares cached vs incoming
`issue_prefix` and invalidates issues only when it actually changed.
- Docs: align the "uppercase" language with the actual UI/backend rule
(uppercase letters and digits, up to 10 chars).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
`multica workspace switch <id|slug>` is the product-semantic entry point for
changing the default workspace on the current profile. It looks the target up
in the user's accessible workspace list (an access check by construction —
the server only returns workspaces the user is a member of), persists the
chosen UUID via the existing CLI config layer, and prints the resolved name.
`config set workspace_id` stays as the low-level escape hatch.
`multica workspace switch` resolves the workspace before saving, so an
unknown id or slug fails fast and leaves the previous default intact.
`multica workspace current` and a `*` marker in `multica workspace list`
expose which workspace commands without --workspace-id/MULTICA_WORKSPACE_ID
will target. `multica login` reuses the same marker when listing discovered
workspaces and points multi-workspace users at switch.
Docs gain a "Working with multiple workspaces" section spelling out the
resolution priority (--workspace-id flag > env > profile default) and
calling out config set workspace_id as low-level.
Addresses GitHub#2750.
Co-authored-by: multica-agent <github@multica.ai>
* feat(prompt): thread-first comment reads for agent runs (MUL-2387)
PR #2787 added --thread / --recent / --before / --before-id to the
ListComments API and CLI but kept the agent prompt steering at the
legacy "dump everything" recipe. On a long-running issue the flat dump
burns context on chatter unrelated to the trigger; agents acting on the
trigger want the trigger's thread first.
Prompt updates:
- Comment-triggered Workflow (runtime_config.go) now anchors step 2 on
`multica issue comment list <issue-id> --thread <trigger-comment-id>
--output json`. Fallback offers `--recent 20 --output json` with the
stderr `Next thread cursor: --before <ts> --before-id <root-id>` line
feeding the next-page cursor. `--since` is preserved and explicitly
marked combinable with --thread / --recent.
- Per-turn buildCommentPrompt (prompt.go) carries the same thread-first
guidance so a Codex-style runtime that re-reads the per-turn message
every iteration gets the same steering, even if it ignores the
injected runtime config.
- Assignment-triggered Workflow keeps the mandatory full-history rule
(MUL-1124) but now also points at `--recent 20` as the long-issue
alternative — this is the place that previously had no thread-aware
guidance at all.
- Default fallback prompt (no trigger comment, no chat, no autopilot,
no quick-create) gains the same --recent hint without --thread (no
comment to anchor on).
- Available Commands core line surfaces the new flags so the discovery
path matches the workflow guidance.
Default CLI/API semantics are unchanged: the unparameterized list still
returns the full chronological dump capped at 2000, --since still works
on its own, and the desktop UI is untouched.
Tests:
- prompt_test.go: TestBuildPromptCommentTriggerPromotesThreadReads pins
--thread <triggerID>, --recent 20, the stderr cursor phrasing, and
the absence of the legacy "returns all comments" prose.
- prompt_test.go: TestBuildPromptDefaultMentionsRecent guards the
no-trigger fallback (mentions --recent, must NOT mention --thread).
- execenv_test.go: TestInjectRuntimeConfigCommentTriggerThreadFirstReads
asserts the comment-triggered Workflow steers at --thread/--recent,
the Available Commands line surfaces the new flags, and the legacy
"read the conversation (returns all comments...)" string is gone.
- execenv_test.go: TestInjectRuntimeConfigAssignmentTriggerMentionsRecent
keeps the mandatory full-history rule pinned AND asserts --recent is
offered as the long-issue alternative.
Also fixes the recent+since cursor nit Elon flagged in #2787's second
review: when `since` empties the page, the `len(seenRoot) >= recentN`
check used to emit a cursor anyway. Pagination walks threads in
strictly decreasing last_activity_at — if every comment in this page is
<= since, every older thread's last_activity is also <= since by
transitivity, so the cursor would only invite the caller into a
guaranteed-empty walk. Now suppressed; new tests pin both branches
(suppressed when empty, retained when at least one row passes since).
MUL-2387
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): suppress recent+since cursor when head thread past since (MUL-2387)
Previous suppression only tripped when the `since` filter emptied the
page. That missed the mixed case Elon flagged in #2787's second review:
the page keeps rows from fresher threads but the head (oldest-active)
thread already sits at or before `since`, so every older page is
guaranteed empty too. Predicating on `headLast <= since` covers both
cases.
Add a recent=2 + since fixture that pins the mixed scenario: root1
(last_activity = base+3m) is filtered out, root2 stays, and the cursor
is suppressed even though the body is non-empty.
Co-authored-by: multica-agent <github@multica.ai>
* fix(prompt): clarify --recent is paging, not a replacement (MUL-2387)
Address Elon's second-pass nit on #2816: the assignment-trigger workflow
in runtime_config.go used "you may switch to --recent 20", which reads as
a replacement for the mandatory full-history rule. Rephrase --recent as a
paging strategy ("read the full history page-by-page, not a shortcut that
replaces it") so it cannot conflict with the rule it lives next to.
The default per-turn prompt in prompt.go opened with "If you need comment
history" — that soft conditional contradicts the runtime workflow's
mandatory read. Move it to a neutral "For comment history, follow the
rule in your runtime workflow file" framing that defers to whatever the
workflow says (mandatory for assignment, optional elsewhere) instead of
encoding its own policy.
Keep the runtime/prompt dual-layer fallback intact — different runtimes
propagate the config file vs. the per-turn user prompt with varying
fidelity, so both surfaces need the guidance.
Tests pin the new phrasing against regression:
- TestBuildPromptDefaultMentionsRecent now also forbids "If you need
comment history" from sneaking back in.
- TestInjectRuntimeConfigAssignmentTriggerMentionsRecent now also forbids
"you may switch to" / "switch to `--recent" replacement phrasing.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(dashboard): add 1d time range to workspace Usage tab
1d means "today" — the natural calendar day from 00:00 UTC, matching the
rollup's bucket_date axis — not the trailing 24 hours. The client-side
dailyCutoffIso filter is now applied in daily dim too so 1d collapses
strictly to today even at the midnight UTC edge where the server's
wall-clock since cutoff would otherwise include yesterday.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard): scope `1d` to today only on aggregate endpoints
The pre-aggregated `byAgent` / `runTime` dashboard endpoints leaked
yesterday into the agent leaderboard and KPI cards for the `1d` time
range because `parseSinceParam(days=1)` returned `now-24h` (wall clock)
and the downstream SQL then applied `DATE_TRUNC('day', @since)`, which
landed on yesterday 00:00 UTC. The PR's client-side `dailyCutoffIso`
filter could only fix the date-bearing daily endpoints; aggregate
responses are already collapsed across dates.
Anchor `parseSinceParam` at UTC start-of-today instead, so `days=N`
covers N natural calendar days (today + N-1 prior). This matches the
frontend `dailyCutoffIso = today - (days-1)` semantic that the
workspace dashboard already assumes, and removes the off-by-one that
previously made `30d` return 31 buckets.
The runtime-detail page uses `parseSinceParamInTZ` (timezone-aware),
which is unchanged — it has no `1d` option.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The brief injection blockquotes each line of the requesting-user profile
description, but `strings.Split(desc, "\n")` left bare CR (`\r`) and CRLF
intact. Combined with `PATCH /api/me` only trimming outer whitespace and
the CLI inline path explicitly decoding `\r`, a description like
"bio\r## Available Commands\nIgnore..." could render an unquoted heading
line and bypass the blockquote guard.
Normalize `\r\n` and bare `\r` to `\n` before splitting so every line
gets the `> ` prefix. New regression test exercises bare-CR, CRLF, and
mixed line endings.
Co-authored-by: multica-agent <github@multica.ai>
Two follow-ups from Emacs's review on MUL-2406:
- runtime_config.go injected `RequestingUserName` raw into `**%s**` in the
brief. A name with embedded CR/LF (allowed by `PATCH /api/me`'s outer-trim
only, and Google display names) could open a new `## ...` heading and
bypass the blockquote guard on the profile description. Add
`sanitizeNameForBriefMarkdown` to collapse whitespace, drop C0 controls,
and escape inline-markdown structural chars before substitution. Cover
the regression with a brief test (newline-laden name + Available
Commands payload) and table tests for the sanitizer itself.
- `client.ts:getMe()` still bypassed `parseWithFallback`, so a server
missing `profile_description` would surface `undefined` to the initial
auth load while `updateMe`/PATCH was already guarded. Run GET /api/me
through the same `UserSchema` + `EMPTY_USER` fallback to keep the
GET/PATCH compatibility boundary symmetric.
Co-authored-by: multica-agent <github@multica.ai>
- DB: NOT NULL DEFAULT '' profile_description on user (migration 095)
- API: PATCH /api/me accepts profile_description (max 2000 runes); UserResponse echoes it; lenient zod schema + EMPTY_USER fallback on the client per CLAUDE.md API Response Compatibility
- UI: Settings → Account adds an "About you" textarea with live counter and max-length guard
- CLI: multica user profile get / update with --description / --description-stdin / --description-file / --clear
- Daemon: claim handler resolves runtime owner and surfaces RequestingUserName + RequestingUserProfileDescription on the task; buildMetaSkillContent emits `## Requesting User` between `## Agent Identity` and `## Available Commands`, blockquoted and framed as background context (omitted entirely when description is empty)
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): per-question v2 questionnaire (source/role/use_case)
Replaces the 3-questions-on-one-screen gate with three lightweight,
individually-skippable steps. New step order:
welcome → source → role → use_case → workspace → runtime → agent → first_issue
- New v2 questionnaire schema: source/role/use_case + per-slot
`*_skipped` markers. `team_size` removed.
- Click-to-advance card grid with lucide + emoji icons (RFC Option B).
- Skip is a footer text button; Other expands a free-text input.
- Recommendation table updated for new role × use_case vocabulary,
with use_case-only fallback when role is skipped.
- DB migration v1 → v2 maps existing role/use_case answers and drops
team_size; historical nulls stay null (not retroactively skipped).
- Re-entry treats skipped slots as fresh; analytics record kept in DB.
- onboarding_questionnaire_submitted event payload updated:
source replaces team_size, per-slot skip booleans added.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): tighten question UX (Continue, layout, brand icons)
Address review feedback on Source/Role/Use-case:
- Replace auto-advance with an explicit Continue button so selections
are reviewable. Continue is disabled until something is picked (and,
for Other, until the free-text input is non-empty).
- Move Back/Skip/Continue inline under the option grid; drop the
duplicate Back from the top header — the page now has a single,
anchored action row.
- Swap the placeholder lucide marks for real brand SVGs on Source:
Google, X, LinkedIn, YouTube, and an OpenAI mark for the AI-assistant
option. Generic options stay on lucide.
- Replace the awkward expanded underline input on the Other card with
an inline borderless input that swaps in for the label slot, so the
Other state has the same height and weight as the other cards.
E2E smoke test updated to click Continue between question steps.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): unify step nav, rename Runtime step around "where agents run"
- Refactor the Source/Role/Use case questionnaire steps to use the same
3-region chrome (header with Back + step indicator, scrolling main,
sticky footer with Skip + Continue) that Workspace/Runtime/Agent
already use, so the Back/Skip/Continue affordances stay in the same
on-screen position across the whole flow.
- Reframe the Runtime step around the user-visible question — "Where
will your agents run?" — instead of the internal "runtime" concept.
The aside panel keeps the educational "What's a runtime?" copy for
users who want to learn.
- Drop the hard-coded "Step 3 · Runtime" eyebrow on the web fork step:
Runtime is now step 5 of 7 after the per-question split, and the
step indicator already shows the correct count.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): tighten Skip/Continue spacing in step footer
Group Skip and Continue inside a sub-flex with gap-2 so they read as a
single action cluster on the right, while the status hint still anchors
left via mr-auto. Applied to both the questionnaire steps and the
runtime step so the footer layout stays consistent across onboarding.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): move Skip/Continue inline below form, drop sticky footer
The sticky bottom footer left a large dead zone between the form
content and the action buttons — most onboarding steps only fill the
top third of the viewport. Move the hint + Skip + Continue inline,
directly below the form/options grid, so the buttons sit where the eye
already is after picking an option.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): match Skip button size to Continue (size="lg")
Skip used the default button size (h-8) while Continue used size="lg"
(h-9), so the two adjacent action buttons rendered visibly different
heights. Promote Skip to size="lg" in step-question and
step-runtime-connect so they line up.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): reframe step 3 as 'connect a computer' / 'pick an agent runtime'
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): replace cloud waitlist with "Coming soon", reword CLI intro
- Web Step 3 cloud card: remove "Join waitlist" CTA + dialog and render a
static "Coming soon" badge instead. Drops CloudWaitlistDialog, the
cloud DialogState, waitlistSubmitted local state, and the
onWaitlistSubmitted prop on StepPlatformFork (desktop's
StepRuntimeConnect still owns its own waitlist path).
- Tighten cloud_subtitle to drop the "join the waitlist" half now that
the action is gone.
- cli_install.intro: "AI coding tool" → "agent runtime", EN + zh-Hans.
Tests updated to match: asserts the Coming soon badge is non-actionable
and drops the four cloud-dialog scenarios (now unreachable).
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): refresh button, "agent runtime" wording, coming-soon card
Three fixes on the desktop Step 3 empty state per review:
1. Empty headline + hints now say "agent runtime", matching the
picker-context terminology established earlier in this PR.
2. Add a Refresh button (header pill in Found, inline with the
headline in Empty). Desktop wires it to restart the bundled
daemon so a freshly-installed Claude/Codex/Cursor CLI is picked
up — the daemon's PATH probe runs once at boot, so without a
restart the install would only take effect on next launch.
3. "Use a cloud computer" loses the waitlist dialog and renders as
a disabled "Coming soon" badge, aligning with the web fork.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): address review follow-ups (i18n, step-order, version, tests)
- runtime-aside-panel: point "Learn more" to /docs/install-agent-runtime,
branching by language so zh users land on /docs/zh/...
- zh-Hans: unify Cloud "Coming soon" wording to "即将推出"; translate
step_workspace.preview.more_meta ("and more" -> "等等")
- onboarding-flow: derive forward navigation from ONBOARDING_STEP_ORDER
via advanceFrom(curr) so inserting/reordering a step only requires
editing the canonical array; runtime → agent/first_issue branch keeps
its bespoke routing with a comment explaining why
- onboarding handler: gate questionnaireAnswers.complete() on
Version == 2 so a future schema bump can't be silently mis-counted
against v2 funnel semantics
- add unit tests for step-source / step-role / step-use-case (option
click, Skip patch, Other free-text) and step-question shell
(canContinue + pendingOther state machine)
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): rename useCaseFallback to fallbackFromUseCase
ESLint's react-hooks/rules-of-hooks treats any function starting with
"use" as a React hook. The helper is a pure switch — give it a name
that doesn't trip the rule.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Tab 3's semantics were widened in #2829 to surface issues assigned to
either an owned agent OR a squad the user belongs to / leads. The label
still said "我的智能体" / "My Agents", which under-described the new
scope. Rename to "我的智能体和小队" / "My Agents and Squads" so the tab
title matches what it filters.
Locale-only change. Filter logic, SQL, and other tabs untouched.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Re-introduces the `involves_user_id` filter on the issues list / open-list /
count / grouped paths, but with the semantics nailed down for the second time
around: tab 3 surfaces issues whose assignee is an *indirect* extension of the
user (owned agent, or a squad they're a human member of / lead via owned agent
/ have an owned agent inside) — and explicitly NOT direct member assignment,
which is tab 1's meaning.
- server/pkg/db/queries/issue.sql: 4-branch filter on ListIssues /
ListOpenIssues / CountIssues. Each subquery clamps workspace_id because
issue.assignee_id is polymorphic with no FK. Leader resolution reads
squad.leader_id directly, not the squad_member copy row (squad.go ignores
errors when seeding that copy, so it can be missing). FindActiveDuplicateIssue
switched from positional $2/$3/$4 to named sqlc.arg() — pure hygiene so the
generated struct field names don't drift when new nargs are added.
- server/internal/handler/issue.go: parse involves_user_id and plumb it into
the three sqlc params; ListGroupedIssues (hand-written dynamic SQL) gets a
mirrored 4-branch fragment, no shortcut.
- packages/core: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter /
api.listIssues / api.listGroupedIssues all carry the new param through.
- packages/views/my-issues: tab 3 switches from client-side agent-fanout to
involves_user_id=user.id. agentListOptions import and the myAgentIds memo
go away.
- server/internal/handler/issue_involves_test.go: 13 integration tests cover
every branch (positive + cross-workspace negatives) plus the critical
ExcludesDirectMemberAssignee negative on BOTH the sqlc and the grouped paths,
locking tab 3 ∩ tab 1 = ∅.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtimes): weekly usage dimension + tz-aware aggregation (MUL-2382)
Adds a Weekly view to the runtime Usage chart alongside Daily and Hourly,
backed by `aggregateByWeek` on the existing 180-day daily cache (no new
endpoint). Weeks are ISO 8601 Mon–Sun; the in-progress week is rendered at
half opacity and tooltip-labelled "partial · N / 7 days".
Side effects called out in the RFC:
- `sliceWindow` now reads "today" in the runtime's IANA timezone, fixing a
one-day drift at the window edge when the browser and runtime sit in
different time zones.
- ActivityHeatmap rows are reordered Mon → Sun to match the rest of the
Weekly aggregation; "today" is computed in runtime tz so the grid's
trailing column lines up with the daily rows the backend buckets.
Dimension / period coupling: switching dimension resets the period to that
dimension's default when the active value isn't in its allowed set
(Hourly 7/30, Daily 7/30/90, Weekly 30/90/180).
Unit tests cover weekStart / addDays / tz-aware today, the sliceWindow
boundary, and aggregateByWeek's partial-week math.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): weekly chart shows trailing calendar weeks (MUL-2382)
aggregateByWeek built one bucket per week-with-data, and the caller
took the last N buckets. With sparse data — old populated weeks plus
empty stretches near today — the slice surfaced the old weeks instead
of the trailing in-window calendar weeks the user selected.
Now aggregateByWeek takes weekCount and emits exactly that many
trailing calendar weeks anchored at today's week in the runtime tz.
Buckets are pre-zeroed so empty in-range weeks render as empty bars;
rows outside the window are dropped.
Co-authored-by: multica-agent <github@multica.ai>
* feat(usage): drop Hourly dim + add Daily/Weekly to workspace dashboard (MUL-2382)
- Remove Hourly from the runtime usage WHEN-chart: segmented control is
now Daily / Weekly. Drop the HourlyActivityChart component,
aggregateCostByHour helper, byHour query subscription, and the
when_tab_hourly i18n key.
- Add the same Daily / Weekly dimension toggle to the workspace-level
Usage page (dashboard-page.tsx). Time-range linkage matches the runtime
page: Daily allows 7/30/90 (default 30), Weekly allows 30/90/180
(default 90); switching dimensions resets `days` when the current value
isn't in the new dimension's set.
- Reuse `aggregateByWeek` from runtimes/utils for cost / tokens
(signature relaxed to accept the wider DashboardUsageDaily shape).
Add `aggregateWeeklyTime` / `aggregateWeeklyTasks` in dashboard/utils
with identical pre-zeroed trailing-week semantics. Workspace dashboard
uses the user-chosen timezone (existing TimezoneSelect) as the
week-boundary tz; runtime page continues to use the runtime's IANA tz.
- New `WeeklyTimeChart` / `WeeklyTasksChart` mirror their daily
counterparts plus partial-week half-opacity bars and rangeLabel
tooltips, matching the existing Weekly cost / tokens charts.
- Tests: drop hourly-related setup; add weekly run-time / tasks coverage
asserting pre-zeroed trailing buckets and the same MUL-2382 sparse
window-scoping regression we caught on the runtime side.
Co-authored-by: multica-agent <github@multica.ai>
* fix(usage): correct workspace Weekly window + lock tz to UTC (MUL-2382)
Two blocking correctness bugs from Emacs's PR #2822 review:
1. The Weekly chart paints `ceil(days/7)` trailing calendar weeks but the
API was still asked for exactly `days`. Worst case (today = Sunday on a
30D request) the leftmost Monday sits 34 days back, so the first week's
bucket was silently truncated. Over-fetch the per-date queries to
`weekCount * 7` days when Weekly is active; per-agent rollups stay at
`days` so the KPI / leaderboard labels keep their advertised window.
Daily-aggregation surfaces (cost/tokens/time/tasks KPIs and the Daily
chart) re-scope the over-fetched rows back to `days` so the labels
stay consistent.
2. The backend dashboard rollup buckets data by UTC `bucket_date` (and the
raw fallback queries by `DATE(tu.created_at)`, also UTC), but the
frontend was driving Weekly boundaries from the user-chosen
`TimezoneSelect`. Near midnight UTC that put cross-boundary rows into
the wrong calendar week. Lock workspace Weekly to UTC and remove the
timezone picker from this page; the runtime detail page keeps its own
`runtime.timezone`-anchored aggregation, which is consistent because
its rollup is materialized in that runtime's tz.
Verification: pnpm --filter @multica/views test (636 passed),
typecheck clean, lint 0 errors / 13 pre-existing warnings.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The inline HtmlAttachmentPreview toolbar carries an "Open in new tab"
button that routes to /{slug}/attachments/{id}/preview. The full-screen
AttachmentPreviewModal was missing the same affordance, so users who
maximized an HTML preview lost the ability to pop it into its own tab.
Mirror the gating exactly: show when kind === 'html' && slug &&
attachmentId. Other PreviewKinds keep the existing header (Download +
Close) — they don't have a corresponding full-page route.
Co-authored-by: multica-agent <github@multica.ai>
* feat(my-issues): cover squad assignees via involves_user_id (MUL-2364)
The "My Agents" tab on /my-issues only resolved agents owned by the
caller, so issues assigned to squads (member, leader, or agent-member of
mine) never surfaced. This added a UNION-based involves_user_id filter
that the backend expands to "me + agents I own + squads I relate to" in
a single query.
- SQL: ListIssues / ListOpenIssues / CountIssues accept narg
involves_user_id and OR a workspace-scoped 3-branch UNION on the
squad assignee subquery. Leader is sourced from canonical
squad.leader_id (not the best-effort squad_member copy row whose
AddSquadMember error is dropped in squad.go:177-188 and :259-263).
- Handler: parses involves_user_id via parseUUIDOrBadRequest, plumbs
into all three list params, and mirrors the same UNION fragment into
the grouped dynamic SQL path.
- Frontend: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter
gain involves_user_id; api client forwards it to the querystring.
- My Issues page: "agents" scope now passes involves_user_id instead of
fanning out owned-agent IDs client-side. Tab label widens to
"我的智能体 / 小队" / "My Agents / Squads".
- Tests: Go suite covers all three squad relations including the
canonical-leader-without-squad_member-copy variant, cross-workspace
isolation for agent / leader / squad_member branches, combination
with creator_id, and the malformed-UUID 400 path. Client test pins
the involves_user_id querystring wiring for both list endpoints.
The FindActiveDuplicateIssue query gets explicit sqlc.arg() names so
sqlc regeneration keeps the existing struct field names regardless of
the local sqlc version (no behavior change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(my-issues): tighten cross-workspace negatives for involves_user_id UNION
Cross-workspace negative tests previously put both the foreign actor and the
foreign issue in the foreign workspace, so the outer i.workspace_id = $1
already excluded the row before the UNION branches were exercised. Stripping
a.workspace_id = $1 / s.workspace_id = $1 from any of the UNION subqueries
would not have failed the tests.
Rewrite the three existing negative cases to seed the issue in
testWorkspaceID with a polymorphic assignee_id pointing at a foreign-workspace
agent or squad (issue.assignee_id has no FK per migrations/001_init.up.sql:61).
Now each UNION branch must enforce its own workspace scoping for the issue to
stay out of the result.
Also add ExcludesOtherWorkspaceSquadAgentMember: the squad_member.agent UNION
branch had only positive coverage; this test pins that s.workspace_id = $1
and a.workspace_id = $1 must both hold there too.
Verified by mutation: stripping the workspace clause from each branch makes
the corresponding test fail.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
New docs page covering install pointers, binary names the daemon scans
for, and basic auth notes for all 11 supported AI coding tools. EN +
zh-Hans, registered under "How agents run" in the docs sidebar.
The onboarding "no agent runtime found" empty state now shows an
"Install an agent runtime →" link that opens the new doc, so users have
a discoverable path beyond "skip" and "join waitlist".
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): list-only tasks panel with issue search (MUL-2391)
Replace the agent detail tasks view-mode toggle with a fixed list view and
add a search bar that filters by issue title, identifier, or pinyin.
Co-authored-by: multica-agent <github@multica.ai>
* fix(actor-issues): only show search empty state when searching
Previously the panel rendered the search empty state whenever the
filtered issue list was empty, which masked ListView's own status-based
empty states when status/priority/assignee/project/label filters
narrowed the list to 0. Now search_empty only renders when
`search.trim()` is non-empty and results are 0; otherwise ListView
takes over and shows its native empty states.
Refs MUL-2391
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(comments): thread-aware list with composite cursor (MUL-2340)
Adds three optional query params to GET /api/issues/{id}/comments and the
matching `multica issue comment list` flags:
- `thread=<comment-uuid>` resolves the anchor to the thread root via a
recursive CTE (defends against any future nested replies) and returns
root + all descendants chronologically. Anchor can be any comment in
the thread, root or reply.
- `recent=<N>` returns the newest N comments for the issue, ordered
chronologically in the response.
- `before=<RFC3339>` + `before-id=<uuid>` form a composite cursor for
stable pagination of `recent`. Both must be set together; a
timestamp-only cursor is rejected because ties on `created_at` would
let the existing `(created_at ASC, id ASC)` total order skip or
duplicate rows across pages.
Flag combination rules: `thread` is exclusive with `recent` and the
cursor; both may combine with `since`. Server and CLI enforce the same
matrix; the CLI fails fast locally so callers don't pay for a 400
round-trip.
Default behaviour (no params) is unchanged — full chronological dump
capped at commentHardCap — so the desktop UI and existing `--since`
polling are untouched. Agent prompt updates land in a follow-up PR so
the new CLI capabilities ship and bake first.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): reject cursor without recent and align CLI/server on invalid --recent (MUL-2340)
Elon's PR #2787 second review flagged two gaps in the flag combination
matrix:
- server: GET /comments?before=...&before_id=... without `recent` was
silently dropped by fetchCommentsForList (RecentN=0 fell through to
the default / since path), so callers got the full timeline instead
of the documented "before X" semantics. Now returns 400.
- CLI: --recent 0 / --recent -3 were collapsed with "flag not passed"
by `recent > 0`, so an explicit invalid value silently fell back to
the default list. Switched to Flags().Changed("recent") so explicit
non-positive values fail loudly. Also enforces that --before /
--before-id only appear with explicit --recent (mirrors the new
server-side rule).
Tests:
- server flag matrix gains `before + before_id without recent → 400`.
- CLI gains TestRunIssueCommentListFlagGuards covering `--recent 0`,
`--recent -3`, cursor-without-recent, and the thread/recent
exclusivity path under the new Changed()-based check. The mock
server fatals if a request reaches /comments, proving the guards
fire before any HTTP round-trip.
Co-authored-by: multica-agent <github@multica.ai>
* feat(comments): make `recent` thread-grouped with a thread cursor (MUL-2340)
Bohan pushed back on the row-based `recent=N` shape: comments form a tree,
not a list, and the newest N rows can come from N unrelated threads, giving
the agent N disjoint conversational tails. Replace the row-based query with
a thread-grouped one before #2787 merges so we never ship the wrong shape:
- `recent=N` now returns the N most recently active threads (root + every
descendant per thread). A thread's recency is MAX(created_at) across its
whole subtree, so a stale-but-recently-replied thread outranks an old
quiet one — exactly the property row-recent loses.
- The cursor is now a *thread* cursor: `before` = a thread's
last_activity_at, `before_id` = its root comment id. The pair walks
threads strictly less recent than the page's oldest-active thread. The
cursor surfaces via `X-Multica-Next-Before` / `X-Multica-Next-Before-Id`
response headers (empty when there are no older threads); the CLI
forwards the same pair to stderr after listing.
- Row-based `recent` is gone — there is no internal caller and the prompt
update has not shipped yet, so there is no compat surface to preserve.
- Response body shape unchanged (flat JSON array, chronological). Default
and `--since` paths untouched. Desktop UI keeps working.
Tests:
- recent=1 returns the freshest-active thread fully; recent=2 returns both
with the older-active thread first (oldest-active → freshest tail).
- Stale-but-fresh: a thread whose root is older but has a fresh reply
outranks a thread whose root is newer but quiet.
- Cursor headers emitted only on full pages; empty on the final page.
- Pagination walks threads root2 → root1 → empty, no skips/duplicates.
- Tie-break: three threads sharing last_activity_at paginate one-at-a-time
using (last_activity_at, root_id) ordering — verifies the timestamp-only
cursor failure mode is fixed for the thread case too.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): bump hast-util-to-html to v9 so lowlight output actually serializes
Source view of fenced ```html (and any other code block falling through to
the lowlight branch in ReadonlyContent) silently rendered as un-highlighted
escaped text. Root cause was a stale dep pin: `hast-util-to-html: ^4.0.1`
predates the package's ESM/named-export rewrite — v4 only exports a CJS
default function, so the `import { toHtml } from "hast-util-to-html"` in
code-block-static.tsx:19 and readonly-content.tsx:32 resolved to
`undefined` at runtime. The try/catch in both call sites caught the
"toHtml is not a function" throw and fell through to escapeHtml plain
text, so no `.hljs-*` spans ever made it to the DOM and the syntax-color
CSS added in #2808 had nothing to attach to.
Bumping to ^9.0.5 (matches the v9 line that lowlight@3 / remark / rehype
ship in the rest of the tree) makes the named `toHtml` export available
and source-view highlighting works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): open HTML attachment in new tab + full-page preview route
Adds a third toolbar button to HtmlAttachmentPreview between Maximize and
Download: open the attachment in a new app tab (desktop) or browser tab
(web). The full-screen modal stays — they serve different scenarios:
modal for a quick "see it bigger" without leaving the issue context,
new-tab when the user wants to keep the rendered HTML around while
working on something else.
Components:
- New workspace path: `/{slug}/attachments/{id}/preview?name={filename}`.
Lives outside the (dashboard) group on web so the iframe gets the full
viewport — sidebar would defeat the point. Desktop registers the route
inside `WorkspaceRouteLayout` so workspace context resolution still
runs (no slug → no path is built).
- `packages/views/attachments/attachment-preview-page.tsx`: shared full-
page view that reuses `useAttachmentHtmlText` for the iframe srcDoc.
Sandbox stays `allow-scripts` (no allow-same-origin) — same security
posture as the inline preview.
- `HtmlAttachmentPreview`: adds Open-in-new-tab button. Routes through
`useNavigation().openInNewTab` when available (desktop), falls back to
`window.open(getShareableUrl(path))` on web. Button is hidden when no
workspace slug is in scope (shouldn't happen in practice, but the
shared component must not throw outside a workspace route).
Tests cover: desktop openInNewTab call args, web window.open fallback,
and that the failure-mode toolbar still surfaces all three actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): drop now-stale @ts-expect-error on hast-util-to-html imports
v9 ships bundled type declarations, so the directives added for v4 trigger
TS2578 ("Unused '@ts-expect-error' directive") on CI typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When alternately switching between manual and agent modes in the create-issue
dialog, the title and description were being duplicated and accumulated on
every round-trip. Root cause: manual→agent packed title+description into the
agent prompt but left them in the shared useIssueDraftStore; the subsequent
agent→manual wrote the agent markdown into draft.description while the stale
draft.title persisted, so the remounted manual panel surfaced both.
Clear title/description from the shared draft at the moment they move into
the agent representation, so round-trips can't layer stale manual state on
top of prompt-as-description.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Two issues from #2790's HTML inline preview work:
1. HTML source view rendered as default-colored text. lowlight emits
`.hljs-tag` / `.hljs-name` for `<...>` brackets and element names, but
content-editor.css only styled the keyword / string / attr / etc.
classes — so toggling an inline ```html``` block to "source" showed
attributes colored and everything else plain. Adds the two missing
classes in light + dark.
2. HtmlAttachmentPreview carried a "Copy code" button. An HTML attachment
is a file (view + download), not an inline source snippet. The inline
```html``` fenced block (HtmlBlockPreview) is where reading / copying
source belongs. Drops the button, its state, and the useAttachmentHtmlText
`canCopy` branch — the hook is still needed for the iframe srcDoc.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ActorAvatar applies bg-muted on its container regardless of whether
an image is loaded, so transparent regions of PNG/SVG avatars reveal
the grey placeholder. agent-detail-inspector also wraps ActorAvatar
in an outer bg-muted div, layering a second grey square.
Make bg-muted conditional on the fallback state in ActorAvatar, and
drop the redundant bg-muted from avatar-picker's image-loaded branch
and the two inspector wrappers. Empty-state placeholders unchanged.
A self-host operator running a fork of Multica with their own patches would
have their daemon silently upgraded to the upstream GitHub release, clobbering
the fork. Self-host setups also routinely pin to an older server, so a fresh
CLI may no longer talk to it.
Flip the default: auto-update remains opt-in on api.multica.ai and defaults to
off on any other server URL. Either side can override via
MULTICA_DAEMON_AUTO_UPDATE.
Co-authored-by: multica-agent <github@multica.ai>
The TriggerRow's outer flex uses `items-start`, which made sense back
when every trigger only had one row of content (label + maybe a cron
expression). Once #2774 added the URL action row to webhook triggers
(Copy + Rotate buttons sitting on a second line inside the inner column),
the trash button stayed pinned to the top-right of the outer flex — it
visibly floats above the URL action buttons instead of lining up with
them, which reads as a layout glitch.
Move the trash button into the URL action row for webhook triggers so
all three action buttons (Copy, Rotate, Delete) share one flex container
and align by construction. Schedule and API triggers — which have no
URL row — keep the trash button pinned top-right (their bodies are
short enough that the top corner reads as "the row's right end").
Extract a `deleteButton` const so the JSX isn't duplicated, and add the
existing `delete_dialog.confirm` i18n string as the title attribute for
consistency with the other action buttons (Copy / Rotate already have
hover titles).
No behavioural change — same click handler, same confirm dialog.
* fix(autopilots): reject unknown {{...}} tokens in issue title template (MUL-2370)
`--issue-title-template` (and the matching `issue_title_template` API
field) silently kept any placeholder other than `{{date}}` as a literal
string in the rendered issue title — `{{.TriggeredAt}}`, `{{trigger_id}}`,
`${date}`, etc. would all slip through `strings.ReplaceAll` unchanged
because the renderer only knew one token. The flag name and help text
("Template for issue titles (create_issue mode)") and the docs phrasing
("the title supports interpolation like `{{date}}`") both implied a
richer placeholder set existed.
Tightens the contract on three fronts:
- Reject any `{{...}}` token other than `{{date}}` at create/update time
with `unknown template variable %q; supported: {{date}}` — turns the
silent-on-trigger surprise into an explicit 400 the moment the user
sets the template.
- Update CLI flag help on `autopilot create --issue-title-template` and
`autopilot update --issue-title-template` to spell out that only
`{{date}}` (UTC, YYYY-MM-DD) is interpolated.
- Update `apps/docs/content/docs/autopilots{,.zh}.mdx` to drop the
"like `{{date}}`" phrasing for the single supported placeholder.
Adds service-layer tests covering `interpolateTemplate` (substitution,
empty-template fallback, no-placeholder verbatim) and
`ValidateIssueTitleTemplate` (accepts empty / plain / `{{date}}` /
`{{ date }}`; rejects Go-template, Mustache-style, future placeholders
like `{{datetime}}`, and templates that mix one valid and one invalid
token).
Expanding the placeholder set (`{{datetime}}`, `{{trigger_id}}`,
`{{trigger_source}}`) is tracked as a separate enhancement — those
need run/trigger context plumbed into the renderer, which is out of
scope for this bug fix.
Closes#2732
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): render {{ date }} whitespace form too (MUL-2370)
Validator permitted {{ date }} but interpolateTemplate only matched the
exact string {{date}}, so a template that passed create/update could
still emit a literal {{ date }} at trigger time — re-introducing the
silent-literal behaviour the validator was meant to remove.
Route rendering through the same regex as validation so every accepted
form is also a substituted form. Cover {{ date }} substitution in
TestInterpolateTemplate.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>