mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
agent/lambda/a0f83366
1255 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5251a0958d |
fix(daemon): bound silent OpenCode streams (#5523)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47e9c8add5 |
fix(web): add macOS Intel downloads to landing page (#5517)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b2097ccbb |
feat(inbox): archived notifications sub-view (MUL-3736) (#5518)
Adds an "Archived" sub-view to the Inbox, reachable from an entry at the
bottom of the main list, with per-row unarchive. Mirrors chat's archived
sub-view so the two surfaces share one mental model.
Backend:
- GET /api/inbox/archived and POST /api/inbox/{id}/unarchive. Kept off the
existing GET /api/inbox so installed clients keep their contract and the
unbounded archive never rides along with the main list.
- The archived query excludes any issue that still has an active row. Archiving
is issue-level, so a new notification on an archived issue leaves old archived
rows beside a fresh active one — without the guard the issue renders in BOTH
lists. The exclusion lives in SQL so neither list depends on the other's cache.
- Unarchive is issue-level (mirroring archive) and leaves `read` untouched, so a
restored unread item raises the unread badge again.
- v1 ships no pagination: LIMIT 200, newest-first, so truncation drops the
oldest rows and never hides a group's newest one.
- inbox:unarchived event, fanned out to the recipient like the other personal
inbox events.
- Two CONCURRENTLY-built indexes; inbox_item previously had none covering
workspace/archived/created_at.
Frontend:
- Separate TanStack cache per list; every inbox event invalidates the workspace
prefix, since any of them can move an item across the boundary.
- View persisted as ?view=archived, so refresh, back/forward, and the mobile
detail-back all return to the list the user was in.
- Batch actions stay main-view only — they archive from the MAIN inbox, so
offering them over the archived list would do the opposite of what it reads.
- Mobile subscribes to inbox:unarchived (its list gains the restored row); its
own archived view remains follow-up.
Known debt: no pagination, so an archive past ~200 rows is truncated silently
in the UI; the entry's count is the deduplicated count of the rows returned.
Verified: pnpm typecheck/test/lint (0 errors), go build/vet, Go inbox suite
against a real Postgres, migrations up+down, and EXPLAIN confirming both new
indexes serve the query.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
849df8bb3b |
fix(desktop): fade tab separators on hover, correct their weight (MUL-4811) (#5500)
The tab strip's hairlines used --border, which is authored for near-white surfaces: its other uses sit on --surface-raised (L=1.0) where it gives a reasonable dL 0.055, but on --app-shell (L=0.964) it collapses to dL 0.019 -- about a quarter of the 0.068-0.080 the rest of the chrome's 1px keylines run at, so it read as almost invisible. Move both the tab separator and the pinned-zone divider to --surface-border, which the surrounding chrome (the active tab's border, the flare arcs, the content card's ring) already uses, and which is what the system draws for separators on --sidebar -- a surface whose lightness is identical to --app-shell. Also fade a hairline out while either tab it divides is hovered, so it no longer lingers 2px off the hover pill's rounded edge. The hairline sits on a tab's own left edge, so the pair's other half belongs to the neighbouring component instance and no ancestor-based variant can reach it; hence the adjacent-sibling custom variant. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
650f933367 |
fix(desktop): rename Linux executable to multica-desktop (#5483)
The Electron desktop app's Linux packages (deb/rpm/AppImage) installed their launcher binary as `multica`, colliding with the Go CLI binary of the same name. Whichever won PATH resolution silently shadowed the other; hitting the Electron binary from a CLI invocation exits 0 with empty stdout (its own Chromium flags eat args like `--output json`), which reads as a healthy but empty CLI response instead of "wrong binary". Rename the packaged executable to `multica-desktop` so `multica` on PATH is unambiguously the Go CLI. StartupWMClass is unaffected since it derives from app.getName(), not executableName. Fixes #5481 Co-authored-by: Product Engineering Specialist <support@weekome.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f8654b37c0 |
MUL-4817: open issue tabs in dedicated windows (#5462)
* feat(desktop): open issue tabs in dedicated windows Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): coordinate dedicated window lifecycle Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1062c13bdc |
docs(changelog): add v0.4.2 release entry (2026-07-15) (#5453)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3cde13768b |
test(desktop): de-flake UpdatesSettingsTab preference-load test (#5460)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
19e52e007c |
MUL-4798: make Inbox notification preference updates atomic (#5451)
* fix(notifications): make preference updates atomic Co-authored-by: multica-agent <github@multica.ai> * fix(notifications): serialize preference mutations Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
54fd29ebdd |
MUL-4383 fix(daemon): stop routing CodeBuddy skills/memory through Claude's .claude paths (#5224)
* fix(daemon): stop routing CodeBuddy skills/memory through Claude's .claude paths CodeBuddy Code is a Claude Code fork but ships its own native config directory (~/.codebuddy, .codebuddy/) with its own memory filename (CODEBUDDY.md). It only reads .claude/skills or CLAUDE.md if a user manually symlinks/copies them during migration (https://www.codebuddy.ai/docs/cli/troubleshooting#migrating-from-claude-code). Multica's daemon/execenv code treated "codebuddy" as an alias for "claude" in three places, so skills synced by Multica landed in .claude/skills/ and CLAUDE.md — paths the default CodeBuddy install never reads — instead of ~/.codebuddy/skills, .codebuddy/skills, and CODEBUDDY.md as documented at https://www.codebuddy.ai/docs/cli/codebuddy-dir and https://www.codebuddy.ai/docs/cli/skills. Split the "claude", "codebuddy" switch cases in: - daemon/local_skills.go (user-level local skill discovery/import) - daemon/execenv/context.go (per-task skill materialization) - daemon/execenv/runtime_config.go (runtime brief target file) Added regression tests locking in the new paths and updated the install-agent-runtime / providers docs (all 4 locales) that had documented the old .claude/skills behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * test(daemon): cover CodeBuddy sidecar hygiene and lifecycle Address PR #5224 review feedback: exclude CODEBUDDY.md/.codebuddy from repo-cache worktrees, extend sidecar lifecycle matrices to codebuddy, and make the local-skills CodeBuddy test exercise a true same-key collision. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
ea03912baf |
perf(desktop,issues): single-router tab sessions (MUL-4741 Phase 2) + trace-driven surface mount/render overhaul (MUL-4474/4750 reland) (#5403)
* Reapply "perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#…" (#5395)
This reverts commit
|
||
|
|
dddcff8744 |
fix(landing): point dashboard CTAs at the real workspace route (#5442)
The four landing "Dashboard" CTAs used href="/" and relied on the proxy bouncing logged-in visitors from the root path to their workspace. Once #5363 kept "/" public on the official marketing host so logged-in users could browse the site (#5326), that bounce stopped — and the CTAs began resolving to the page the visitor was already on, so clicking them did nothing at all. Resolve the destination in the CTA instead of leaning on a redirect side effect elsewhere: useDashboardCtaHref() runs the same resolvePostAuthDestination() that RedirectIfAuthenticated already uses, so "where the dashboard lives" has one source of truth. This also collapses the same `user ? "/" : "/login"` line that had been copy-pasted across four files. proxy.ts and redirect-if-authenticated.tsx are untouched — #5363's behavior is correct and proxy.test.ts still passes unchanged. Closes MUL-4794 Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7f12380f05 |
feat(cli): add workspace create command (#5062)
Adds `multica workspace create` (--name/--slug/--description/--context/--issue-prefix, JSON/table output). Creation does not switch the current workspace. Both --name and --slug are required to match the server contract, and the slug is immutable after creation. Docs (en/zh/ja/ko) and the CLI reference now show the executable command. Closes #5055 |
||
|
|
66316c2614 |
fix(grok): harden ACP authentication and capabilities (#5440)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5b26b99722 |
MUL-4164: support Intel macOS Desktop packages (#5436)
* feat(desktop): support Intel macOS packages Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): scope Intel macOS rollout Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
06d79c1750 |
feat(agent): add Grok Build CLI as an ACP runtime (#5285)
Add xAI Grok Build (`grok`) as a first-class Multica runtime over ACP (`grok --no-auto-update agent --always-approve stdio`), reusing hermesClient like traecli and kimi. Includes daemon discovery, protocol_family migration 174, model discovery, MCP passthrough, thinking effort, frontend branding, and product docs. Follows xAI's documented headless ACP flow: after `initialize`, read the advertised `authMethods` and send `authenticate` (preferring xai.api_key when XAI_API_KEY is set, else the cached login token) before any session operation — a real, logged-in CLI rejects session/new and session/load without it. Model discovery performs the same handshake so it returns the live catalog instead of the static fallback. `--no-auto-update` is passed as a global flag (and kept daemon-owned in the blocked custom-arg set) so a background update check can't stall an unattended ACP task. Thinking effort uses the current `--effort` flag, and the minimum grok version is 0.2.89 (ACP + authenticate + session/load + session/set_model + MCP + --effort). Closes #2895 |
||
|
|
b85bb71a58 |
feat: custom issue properties — typed workspace-defined fields with list-surface support (MUL-4463) (#5335)
* feat(server): custom issue properties — definitions, typed values, CLI (MUL-4463)
Workspace-level property definitions (issue_property table; 7 types:
text/number/select/multi_select/date/checkbox/url) plus a typed value bag
on each issue (issue.properties JSONB keyed by definition UUID, mirroring
the metadata machinery: single-key atomic writes, 16KB cap, GIN index).
- Definitions: owner/admin only; agent actors rejected (agents propose,
humans confirm). 20 active per workspace, 50 options per select,
reserved built-in names blocked, archive instead of delete.
- Values: any member or agent; per-type validation with self-correcting
error messages that enumerate legal option ids.
- API: /api/properties CRUD + PUT/DELETE /api/issues/{id}/properties/{propertyId};
issue responses always emit the properties bag.
- CLI: multica property list/get/create/update/archive/unarchive and
multica issue property list/set/unset with name→id translation.
- Events: property:created/updated, issue_properties:changed.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom properties settings tab + issue sidebar editors (MUL-4463)
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 1 — mobile CI, option guard, mutation safety, schema tolerance
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): renumber issue properties to 179 and build the GIN index concurrently
main's migration sequence advanced twice under this PR (167 collision, then
an upstream renumber wave that claimed 178), so issue properties now sits at
179 — verified against main's current tip by the prefix-uniqueness lint.
The properties GIN index moves to its own single-statement migration (180)
using CREATE INDEX CONCURRENTLY — a plain CREATE INDEX on the hot issue
table would block writes for the duration of the build. Mirrors the
119_user_created_at_index pattern; full-chain dry-run on a fresh database
passes through 180.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom-property list surfaces — filter, cards, sort, board grouping (MUL-4463 M2)
Brings custom properties to the issue list surfaces on top of the M1
definitions/values core:
- Filter: per-definition sections in the Filter dropdown (select /
multi_select options with color dots and counts; checkbox as Yes/No
pseudo-options). OR within a definition, AND across definitions;
client-side in applyIssueFilters, mirrored into filterAssigneeGroups
for the assignee-grouped board. Included in active-filter count and
Clear all.
- Cards: per-property Display toggles (cardPropertyIds) render value
chips on board cards and list rows via CustomPropertyValueDisplay.
- Sort: SortField gains property:<id> for number/date definitions.
Server keeps position order (fixed sort enum); the surface controller
re-sorts client-side, swimlane/gantt reuse the same comparator.
Date-only strings compare lexically; missing values sort last.
- Board grouping: IssueGrouping gains property:<id> for select
definitions — one column per option (definition order) plus a
trailing No-value column, option-colored headings. Drag-drop moves
position via UpdateIssue and applies the value through
useSetIssueProperty/useUnsetIssueProperty (properties are not part
of UpdateIssueRequest). Stale persisted property groupings fall back
to status columns.
View-store: propertyFilters + cardPropertyIds persisted via the
partialize allowlist; clearFilters resets property filters; new fields
deep-merge cleanly into pre-existing persisted snapshots.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 2 — desc sort, option bucketing, archived-state reconciliation
- sort: direction now applies to value comparison only; issues without a
value sort last in BOTH directions (the whole-array reverse flipped them
to the front on desc). Test covers the desc+missing case.
- board: values referencing an option removed from the definition bucket
into the No-value column instead of vanishing (unmatched column ids
dropped the issue entirely). Defense-in-depth behind the new server-side
in-use guard; drag-utils test locks both behaviors.
- controller: persisted propertyFilters keyed by archived/deleted
definitions are stripped before reaching the filter predicates, and a
persisted property sort on a non-active definition degrades to manual
order — previously both kept silently applying while the header claimed
otherwise. The filter badge counts only active-definition filters.
Co-authored-by: multica-agent <github@multica.ai>
* feat(properties): server-side property filtering and sorting on list endpoints
Property filter/sort now execute in the database, so results are correct
across the full issue set — not just the loaded 50-per-status window
(closes MUL-4493 item 1's filter/sort half; requested on MUL-4463).
- New `properties` query param on ListIssues and ListGroupedIssues:
JSON {definitionId: [values]} compiled to an AND-of-ORs containment
check (double NOT EXISTS over jsonb_array_elements). One value expands
to every storage shape it could match — string (select), array element
(multi_select), boolean (checkbox) — so the handler stays type-agnostic.
Guarded at 20 definitions / 50 values.
- `sort=property:<definitionId>` resolves the definition and orders by a
typed expression (numeric CASE cast for number, NULLIF text for
date/text/url); missing values sort last in both directions. Malformed
ids 400; unknown/archived definitions degrade to position order instead
of breaking stale clients.
- Frontend: the property filter and property sort ride the IssueSortParam
window bag, so every surface (workspace + my-issues variants), query
key, and per-status load-more page carries them automatically. The
client-side re-sort layer is gone; applyIssueFilters keeps its property
predicate as an optimistic-update backstop.
- Regression test seeds 55 issues and proves a match at position 55 is
returned by a filtered 50-row page, plus sort order/missing-last,
AND-across-definitions, and the 400/fallback sort paths.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address review round 3 — cache reconcile, merged-scope order, GIN-indexable filter, pool loader
- Cache reconciliation: property value writes (mutation settle + WS event)
now invalidate every issue window whose server-side shape depends on
property values — queries filtered by `properties` or sorted by
`property:<id>` (detected via query-key predicate), covering flat lists,
assignee groups, and my-issues variants. Windows without property params
keep the cheap in-place patch. Fixes stale ordering/membership/counts
under staleTime:Infinity.
- My Issues "All" scope: merged assigned/created/involves results are
re-sorted with a comparator mirroring the server ORDER BY semantics
(including property sorts and missing-last, created_at DESC tiebreak) in
both the flat and assignee-grouped merge paths — relation concatenation
no longer overrides the user's sort.
- Filter predicate rebuilt as plain bind-parameter containment ORs
(AND across definitions): EXPLAIN now shows BitmapOr over
idx_issue_properties_gin (the correlated jsonb_array_elements form
defeated the index). Alternatives capped at 256 bind params.
- Property-grouped board gains a pool loader strip: one sentinel per
status that still has server rows, keeping every issue reachable until
per-column pagination lands (MUL-4493).
- Windowing regression test hardened: explicit positions + an assertion
that the unfiltered first page excludes the target (the old fixture tied
at position 0 and the created_at DESC tiebreak put the target on page
one, proving nothing).
- Rollback safety: /api/properties 404 (old server) degrades to an empty
catalog instead of a query error, which also keeps property params from
ever being sent to pre-property servers; migration 179's CHECK
constraints switch to NOT VALID + VALIDATE so the exclusive lock is
instantaneous.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): harden concurrency and cache coordination from clean-room review
Backend (MUL-4762 F1/F4/F5):
- withPropertyLock: pg_advisory_xact_lock helper; definition create/update
and value writes now serialize config-vs-value and cap-vs-insert races
(workspace-level 'props:' lock + per-definition 'prop:' lock, ordered).
- propertySortExpr degrades archived definitions to position sort.
Frontend (F2/F3/F6):
- onIssuePropertiesChanged invalidates plain assignee-group caches too.
- Property value mutations cancel list refetches in onMutate and roll back
only the touched key against the current bag (concurrent WS writes to
other keys survive a failed write).
- useUpdateIssue reconcile drops the stale properties bag from the server
snapshot; the property pipeline owns that field.
- Surface controller passes persisted property filters/sorts through
until the catalog query settles (cold cache no longer strips them).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): open_only branch honors the properties filter
ListOpenIssues takes the parsed AND-of-ORs containment groups as a single
jsonb properties_filter param and unrolls them with a static double
NOT EXISTS; previously the open_only path parsed the properties param and
silently dropped it (clean-room review F7a).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): toast on failed board drag to a property column
Property-column drags rolled the card back silently on failure; mirror
the status/assignee drag path (use-issue-surface-actions) so the
snap-back is explained (clean-room review F3, drag half).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
7468ce06be |
fix(desktop): match canvas left margin to right when sidebar is collapsed (#5430)
The shell canvas kept its static 2px left margin after the left sidebar collapsed offcanvas, while the right edge uses an 8px margin. Animate the left margin to 8px (same spring as the rest of the shell) whenever the sidebar leaves the main flow, so the floating canvas sits symmetrically. Fixes MUL-4780 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b4d690664 |
MUL-4744: restore Autopilot webhook response contract (#5397)
* fix(autopilots): restore webhook response contract Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): preserve run id on webhook retries Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ebca1c1914 |
docs(changelog): add 0.4.1 release notes (en/zh/ja/ko) (#5394)
* docs(changelog): add 0.4.1 release notes across en/zh/ja/ko Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): cancel scheduled update checks when auto-update is disabled The startup and periodic update timers were left running when a user turned automatic updates off; the timer callbacks only consulted the preference asynchronously, so a tick that raced the preference flip could still fire a check. Cancel the timers on disable (and re-arm them on re-enable) so disabling truly stops future background checks, removing a CI-flaky race in updater.test.ts. Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): drop issue-view virtualization improvement from 0.4.1 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
de98b7cb83 |
test(desktop): stabilize updater preference test against slow-disk race (#5392)
The "skips startup and periodic checks when automatic updates are disabled" case advanced fake timers without awaiting the async preference load. On slow CI the in-flight readFile resolved after afterEach() removed the temp dir, defaulted enabled back to true, and fired a deferred background check into the next test's freshly-cleared shared mock — making "persists the automatic update preference and stops future background checks" flake with checkForUpdates called once. Await updater:get-preferences (which awaits preferencesReady) before advancing timers so the read settles against the existing file and no background work outlives the test. Test-only change; production behavior is unaffected. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e07b5403ab |
MUL-4502: make autopilot webhook admission durable (#5386)
* fix(autopilots): make webhook admission durable Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): address webhook delivery review Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2d13b26fcc | feat(desktop): add automatic update preference (#5380) | ||
|
|
c27919a4d0 |
feat(agents): add DevEco Code (deveco) runtime agent (MUL-4050) (#4916)
Adds DevEco Code (Huawei's HarmonyOS coding agent, built on the OpenCode engine) as a first-class runtime provider: backend/model parser, daemon discovery (probe + login-shell list + Windows .cmd native resolver), runtime profile migration (protocol_family whitelist), provider UI, metrics, and four-language docs. MCP injection is deferred (UI gates it off). Migration numbered 175. |
||
|
|
ef9b334408 |
MUL-4398: fix Hermes bound-skill discovery with per-task overlay (#5308)
* fix(execenv): overlay per-task HERMES_HOME so Hermes discovers bound skills Hermes has no workspace-relative skill discovery — it scans <HERMES_HOME>/skills first, then skills.external_dirs from config.yaml (verified against the bundled agent/skill_utils.py). The daemon wrote assigned skills to the generic .agent_context/skills/ fallback, which Hermes never reads, so they silently never took effect (#5242). When (and only when) an agent has skills bound, redirect HERMES_HOME to a minimal per-task compatibility overlay; a skill-less Hermes task keeps its real home and original behavior: - mirror every top-level entry of the shared home via symlink except the overlay-owned ones (denylist), reconciling entries deleted from the shared home; - derive a task-local config.yaml whose skills.external_dirs references the shared skills dir plus the user's existing external_dirs, expanded against the sanitized effective child env (unknown vars preserved, blocklisted keys resolved to the process value) and normalized to absolute paths; - write only the bound skills into the task-local skills/ dir (home skills scanned first, so they win); global skills are referenced, not copied; - keep memories/ overlay-owned (fresh per-task dir) AND disable the external memory.provider, so neither on-disk memory nor a shared backend crosses tasks; - keep active_profile/profiles out of the overlay so Hermes can't follow a sticky profile and redirect past it at startup. Profile handling mirrors hermes_cli.profiles: the daemon reads -p/--profile with agent.HermesProfileFromArgs and seeds the overlay from that profile's home via ResolveHermesSourceHome (default/invalid -> base, valid name -> <base>/profiles/ <name>, validated; a missing named profile fails closed). The profile flags are stripped from the acp argv ONLY when the overlay is active (hermesLaunchArgs), so a skill-less task's profile passes through unchanged. HERMES_HOME is no longer custom_env-blocklisted: no skills -> user value passes through; skills -> overlay overrides after layering. Fail closed — Prepare errors, Reuse returns nil. Task home 0700, derived config 0600 via atomic replace. Platform-native default home (%LOCALAPPDATA%\hermes, incl. the LOCALAPPDATA-missing fallback, on Windows). Tests span execenv/daemon/agent: no-skill no-op, child-env layering + env sanitization, profile parse/unquote + conditional strip + final args/env per scenario, custom/profile/default/invalid/missing/Windows source home, sticky- profile not mirrored, memory dir isolation + external provider disable, mirror reconciliation, external_dirs rebasing + sanitized/unknown-var expansion, local-precedence slug, perms, fail-closed, resume teardown. Docs (en + ja/ko/zh). Fixes #5242 * fix(hermes): make profile selection one resolver contract matching Hermes Round 5 review: the profile chain approximated Hermes' semantics in three separate places (argv parsing, source-home selection, arg filtering), so it diverged from native Hermes in several merge-blocking cases. Collapse it into one authoritative resolution: - agent.ParseHermesProfileArgs replaces HermesProfileFromArgs/ FilterHermesProfileArgs. It reproduces _apply_profile_override step 1/1b (first occurrence, value-flag skipping, `--` and `mcp add --args` boundaries, space-form profile-id guard) and returns the exact argv occurrence to consume; StripHermesProfileArgs removes only that occurrence. - execenv.ResolveHermesProfile replaces ResolveHermesSourceHome. It derives the Hermes root exactly like get_default_hermes_root (an already-profile-scoped HERMES_HOME roots at its grandparent), selects an explicit profile first, otherwise trusts a profile-scoped home (step 1.5) and only then the sticky <root>/active_profile (step 2), and validates via normalize/validate_profile_name (reserved hermes/test/tmp/root/sudo and empty inline `--profile=` are hard errors). Profiles always resolve under the root, so `-p default` re-roots and `-p <sibling>` is a sibling, never nested. - The daemon runs one parse + resolve, fails the task closed on a reserved/ invalid selection (matching Hermes' sys.exit(1)), and exports the selected source home as the effective env's HERMES_HOME so ${HERMES_HOME} in a profile's skills.external_dirs expands against the selected profile home (as native Hermes does before loading config.yaml), not the root or the overlay. Regressions added: root + sticky named profile selection; already-profile-scoped home with no flag; that home with -p default and -p <sibling>; reserved and empty inline profile values; and a selected profile whose external_dirs contains ${HERMES_HOME}. * fix(hermes): overlay-owned derived .env + symlink-resolved root Round 6 review, two remaining overlay-bypass paths: 1. A source `.env` could redirect HERMES_HOME after profile resolution. Hermes runs `_apply_profile_override()` then `load_hermes_dotenv()`, which loads `<HERMES_HOME>/.env` with override=True — so a mirrored source `.env` carrying an out-of-band `HERMES_HOME=` overwrote the overlay's home, repointing skill discovery and memory back at the source. `.env` is now overlay-owned and DERIVED (writeDerivedHermesEnv): it preserves the source's credentials/settings but strips any `HERMES_HOME` assignment and pins `HERMES_HOME` to the overlay last (single-quoted, literal), written 0600 via atomic replace. It is written even when the source has none, so Hermes' project-`.env` fallback (override=True only when no user `.env` loaded) can't relocate the home either. 2. Root derivation was lexical-only, diverging from `get_default_hermes_root`, which compares `env_path.resolve()` with `native_home.resolve()`. A HERMES_HOME symlinked into `<native>/profiles/<x>` was treated as its own root, so `-p default`/`-p <sibling>` resolved wrong. `hermesRootFromHomeFor` now resolves symlinks (Path.resolve(strict=False)-style best effort) for the containment decision while keeping the returned root unresolved, matching Hermes. Regressions: source `.env` with HERMES_HOME replayed through the override=True dotenv order (bound skill + task memory stay on the overlay; creds preserved); minimal overlay `.env` created when the source has none; and a symlinked profile home resolving `-p default`/`-p <sibling>` to the native root. |
||
|
|
5b57a8ebca |
MUL-4460: fix(email): add SMTP_FROM_EMAIL for SMTP sender
Closes MUL-4460 |
||
|
|
e1d0d68c53 |
Fix public host root redirect (#5363)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
014eb0496d |
docs(changelog): v0.4.0 release notes (#5339)
* docs(changelog): add v0.3.44 release notes across all locales Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): bump release version to 0.4.0 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
41b3045efa |
MUL-4424: bound Codex app-server startup RPCs (#5319)
* fix(codex): bound app-server startup RPCs Co-authored-by: multica-agent <github@multica.ai> * test(codex): de-flake bounded-handshake test The single 500ms handshake bound was shared by the successful preamble RPCs, so a slow fork/exec of the /bin/sh fake app-server could make initialize spuriously time out under parallel load. Raise the test bound to 3s (still below the 5s semantic timeout and 10s harness ceiling) and loosen the elapsed assertion to match. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
220fa58264 |
fix: guide SSH installs to token login (#5318)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e1bf6cc51 |
feat(desktop): merge active tab into content surface, Chrome-style (#5314)
The active tab now shares the content card's fill and keyline: rounded top corners, concave bottom flares (radial-gradient corner pieces whose 1px arc hands the tab border over to the card's top ring), and a borderless base that runs into the card so the two read as one surface. Inactive tabs sit flat on the shell with an inset hover pill and hairline separators that hide around the active tab, Chrome-style. Closes MUL-4439 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bf288349f6 |
feat(project): add start_date and due_date fields (MUL-4388) (#5313)
Projects become schedulable planning objects alongside their issues: add optional start_date / due_date, mirroring issue.start_date / issue.due_date. This is only the first slice of #5227 — labels, metadata, and the editable metadata UI are still out of scope. - migration 166: two nullable DATE columns on `project` (calendar days, no FK/index — matches the issue end-state after migration 112) - sqlc CreateProject / UpdateProject carry the dates; UpdateProject uses narg so an explicit null clears - handler: parse YYYY-MM-DD (400 on bad format), rawFields-presence clear on update, and the hand-scanned SearchProjects query returns the columns - CLI: `project create/update --start-date/--due-date` (empty clears on update) - frontend + mobile types/zod schemas: the two new schema fields are nullable().default(null) so a project from an older backend (frontend deploys before backend) parses to null instead of degrading the batch to the empty fallback; added a search schema drift test - projects skill / CLI docs Part of #5227 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a19e60a9e6 |
feat(chat): support images/files in agent chat replies (MUL-4287) (#5164)
* feat(chat): support images/files in agent chat replies (MUL-4287) Agents can now attach images/files to their chat replies, matching how comment attachments already work. The write-side gap was that the assistant chat_message is synthesized server-side from the completion callback's text output and never bound any attachments. Backend: - migration 150: nullable attachment.task_id (+ partial index), the transient handle that ties an agent's in-run upload to the reply it produces. - POST /api/upload-file accepts task_id: gated to the task's own agent, in this workspace, on a chat task; tags the row with task_id + chat_session_id. - CompleteTask (chat branch) binds the task's still-unclaimed attachments to the assistant message via BindChatAttachmentsToMessage (rejects rows already owned by an issue/comment/chat_message). An empty-output reply that produced files still creates a message so the images have an owner. FailTask binds nothing. CLI: - `multica attachment upload <path>` uploads a file for the current chat task (task from MULTICA_TASK_ID or --task) and prints id / markdown_url / a ready-to-paste markdown snippet. Prompt: - web/mobile chat prompt tells the agent how to attach a file to its reply. Mobile: - chat:done handler now always invalidates the messages list so attachments (absent from the event payload) refetch; mirrors web's self-heal. - chat bubbles render standalone attachment cards via the existing CommentAttachmentList (dedup vs inline references), matching web. Web/desktop needed no change — they already render message.attachments inline and via AttachmentList, and self-heal on chat:done. Tests: upload permission/isolation, bind-on-complete, empty-output+attachments, FailTask no-bind, null task_id untouched, already-owned not stolen, CLI output contract, mobile refetch-on-done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review blockers on chat reply attachments (MUL-4287) Two final-review blockers on PR #5164: 1. Mobile inline dedup only checked raw `url`, so an attachment referenced inline via `markdown_url` (exactly what the CLI snippet emits) rendered twice — once inline, once as a standalone card. Reuse the core `contentReferencesAttachment` helper so dedup covers every real reference form (stable /api/attachments/<id>/download path, url, download_url, markdown_url), matching web's AttachmentList. Extracted the filter into a pure `lib/attachment-dedup.ts` so it is unit-testable, and added a regression test covering `content` containing `attachment.markdown_url` (plus the other URL forms and same-identity sibling dedup). 2. CLI `attachment upload` emitted `![...]` image markdown for every file, producing a broken-image snippet for non-images. Emit image markdown only for image/* content types and a plain link otherwise, with a CLI contract test for both. Approved scope otherwise unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): renumber attachment_task_id migration 150 -> 157 after main merge (MUL-4287) Merged latest main; main renumbered its migrations and now occupies 150-156, so 150_attachment_task_id collided with 150_agent_task_coalesced_comments and would fail TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Renamed to the next unique prefix (157). No content change; migrate up applies cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): render agent-produced files as attachment cards, not raw links The chat upload command handed the agent a bare `[name](url)` markdown snippet. Pasted mid-sentence it renders as a plain text link (not a card), and the referenced URL hides the auto-bound standalone attachment — so a file the agent produced could end up showing as nothing. Return the block-level `!file[name](url)` card syntax instead (images keep `` inline), and markdown-escape the filename so names with `[`/`]` don't truncate the label. The prompt and CLI help now state the file auto-attaches below the reply and the snippet is optional, only for placement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): soften message-list scroll fade (32px → 16px) The 32px edge fade washed out full-bleed content (HTML / image previews) at the list edges. Halve the fade distance so it barely grazes previews while still hinting at more content above/below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): renumber attachment_task_id migration 157 -> 158 main landed 157_agent_task_delivered_comments while this branch was open, colliding on prefix 157 and failing TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Bump this PR's migration to the next free prefix (158). Rename only; the migration body (nullable attachment.task_id + partial index) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): pin attachment upload to the token's task; build index concurrently Two code-review findings on the chat-attachment path (MUL-4287): - Isolation/privacy: POST /api/upload-file only checked the form task_id belonged to the caller's agent, not that it matched the task-scoped token's authoritative X-Task-ID. A run authorized for task A could tag an attachment onto task B (another chat task of the same agent, possibly another user's session), binding it into that reply on completion. Require the form task_id to equal the server-set X-Task-ID; add a same-agent/other-task 403 regression. - Migration: split the task_id lookup index into its own migration (159) built with CREATE INDEX CONCURRENTLY (repo convention) — it cannot share a multi-command file with the ADD COLUMN in 158. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): enforce task-token source on attachment upload; drop transient task_id FK (MUL-4287) Addresses the two remaining Preflight BLOCKERs on PR #5164. Security (file.go): the task_id upload path compared the form task_id to X-Task-ID but did not require X-Actor-Source=task_token. A normal JWT/mul_ PAT leaves that header empty and the middleware does NOT strip a client-forged X-Task-ID; resolveActor's fallback accepts a valid X-Agent-ID+X-Task-ID pair. So a member who learned a task ID could forge both and inject an attachment onto another chat task's assistant reply (cross-session/privacy leak). Now the branch requires X-Actor-Source=task_token first (mirrors chat_history.go's load-bearing boundary), then pins to the middleware-injected X-Task-ID. Tests now go through the real task-token headers and add a forged-JWT-403 regression. Migration (158): task_id is a transient binding handle (written once at upload against an already-validated task, read only during that task's own completion; durable owner is chat_message_id). There is no app-layer path that hard-deletes agent_task_queue rows, and orphan uploads are already reaped by attachment.chat_session_id's ON DELETE CASCADE — so an FK here would only add a cascade dependency the app never relies on plus write overhead on the hot attachment table. Drop the FK; task_id is now a plain UUID column. Added a regression test that an unbound task-tagged upload is reaped on chat_session delete. Index (159, CONCURRENTLY) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(mobile): align !file card preprocess with web parser + CLI escaped labels (MUL-4287) Howard final-review blocker: mobile's `!file[...]` preprocess didn't keep up with the CLI's file-card output, so agent-produced non-image files rendered nowhere on mobile. - `FILE_LINE_RE` used `[^\]]+` for the label, so the CLI's escaped-bracket output `!file[a\]b.pdf](url)` (cmd_attachment.go escapeMarkdownLabel) never matched — the line stayed literal AND `standaloneAttachments` still hid the fallback card (the URL is in `content`), so the file showed nowhere. - Align the matcher with web's `packages/ui/markdown/file-cards.ts`: label allows backslash-escaped metacharacters (ReDoS-safe class), and the URL is restricted to the same allowlist (site-relative /uploads + /api/attachments/ <UUID>/download, plus absolute http(s)); disallowed schemes stay plain text. - Unescape the label to the real filename, then re-escape only the chars that would break a markdown LINK label (mobile emits `[📎 name](url)`, re-parsed by the renderer — unlike web's HTML data-filename), so a raw `]` never truncates the link text. No dedup change: once the inline `!file` renders, hiding the standalone card is correct. Added focused unit tests covering the escaped-label case, parens/ backslash unescape, the site-relative URL form, and disallowed-scheme rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2a48ffa2aa | fix(runtimes): simplify local machine list row (#5298) | ||
|
|
b47e835d7d | refactor(runtimes): organize runtime management by machine (#5297) | ||
|
|
1427e8abd3 | feat(agents): add conversational creation studio (#5296) | ||
|
|
b64ebd60b5 | feat: add customizable keyboard shortcuts (#5294) | ||
|
|
12e3c393d7 | Add auto-save confirmation toasts (#5261) | ||
|
|
d51a3cbbbd | Unify settings layout and auto-save (#5257) | ||
|
|
ca46fdb483 |
fix(ui): lighten menu shadows with dedicated --menu-shadow token (#5256)
Dropdown, context menu, select, and popover surfaces used --floating-shadow, which is sized for window-level overlays and reads too heavy on trigger-anchored menus. Introduce a lighter --menu-shadow tier (surface < menu < floating) and drop the shadow-lg override on ContextMenuSubContent so submenus match their parent menu. Dialogs and sheets keep --floating-shadow. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4efcfb96e3 | feat(ui): establish surface system (#5248) | ||
|
|
e10bb3fff0 |
fix(chat): unify unread badge counting across sidebar, mobile, and thread list (MUL-4286) (#5239)
The aggregate chat unread badge used two different definitions: web/ desktop's sidebar summed unread messages while mobile's tab badge (and the since-removed ChatFab badge it mirrored) counted sessions — the same account state showed e.g. 5 on web and 2 on iOS. The sidebar also summed ALL sessions while the thread list zeroes the row being viewed, so a reply landing in the open conversation flashed a sidebar count with no matching row. - packages/core/chat/unread.ts: countUnreadChatMessages() as the single shared definition (IM-style message total, optional viewed-session exclusion); pure so mobile can import it. - app-sidebar: use the helper and exclude the actively-viewed session, but only while a chat surface is actually showing it (chat route or floating window open) — a remembered selection with both surfaces closed still counts, since nothing will auto mark-read there. - mobile: tab badge switches to the shared message count (99+ cap like the sidebar), ChatSessionSchema parses unread_count, and markRead's optimistic patch zeroes unread_count alongside has_unread. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
53f05cca5e |
feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) (#5218)
* feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) One GitHub App installation can be bound to several workspaces (#4855), but pull_request and check_suite webhooks were still routed to a single workspace via resolveWorkspaceForRepo (workspace.repos registry + oldest-binding fallback). Every workspace but one silently received nothing for a shared repo, with no way to opt in. Deliver each repo event to every workspace bound to the installation. Repo scope is whatever GitHub authorized the installation for; we no longer gate on the workspace.repos registry (that list means "code the agent clones", not a webhook subscription). Each workspace independently mirrors the PR, auto-links against its own issue prefix + github toggles, records check suites against its own PR mirror, and gets its own realtime broadcast. - Extract mirrorPullRequestForWorkspace / recordCheckSuiteForWorkspace and loop over all installation bindings instead of resolving one workspace. - Remove the now-dead resolveWorkspaceForRepo / repoIdentityFromURL routing. - Replace the registry-routing tests with PR + check_suite fan-out tests. Co-authored-by: multica-agent <github@multica.ai> * test(github): out-of-order fan-out test + drop dead query + document multi-workspace delivery (MUL-4343) Addresses review feedback on the webhook fan-out change: - Add TestWebhook_CheckSuite_OutOfOrderFansOutToBoundWorkspaces: a check_suite that arrives before the PR must stash a pending row per bound workspace, and each workspace must drain its own row when the PR fans out. - Remove the now-unused ListWorkspacesWithRepos query (its only caller was the deleted resolveWorkspaceForRepo) and regenerate sqlc; fix the stale "picks the target workspace via the repos registry" comment on ListGitHubInstallationsByInstallationID. - Document multi-workspace event delivery in the GitHub integration docs (en + zh), including an explicit self-host upgrade note: delivery is now keyed on the GitHub connection, so a workspace that relied on the code-repository list alone (without connecting GitHub) must connect the installation to keep receiving events. This is an intentional, documented behavior change — the PR description's earlier "single-binding behavior is unchanged" claim was inaccurate and has been corrected. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
37b0a6e72e |
docs(changelog): add v0.3.43 release entry (MUL-4376) (#5216)
Add today's user-facing release notes to /changelog across en/zh/ja/ko: new Codex gpt-5.6 models, issue-key autolinks, avatar cropping, per-thread agent replies, round avatars, direct batch status, bounded background log, and Lark/Cursor/Claude fixes. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
595f785ac9 |
fix(desktop): make overflowing tab additions visible (#5215)
* fix(desktop): animate overflowing tab additions
* fix(desktop): recalculate tabs after pin layout changes
* docs: document daemon log rotation settings
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* Revert "docs: document daemon log rotation settings"
This reverts commit
|
||
|
|
cb87dd106b |
feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) (#5195)
* feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) Direct (web/mobile) chat no longer uses the last-assistant-row as an implicit input cursor. Each direct send now owns an immutable input batch: - agent_task_queue.chat_input_task_id makes a task the owner of the user messages it must consume; the send path creates the task + user message + attachment bindings + session touch in one transaction, and the daemon is notified only after commit. A claim reads exactly that batch, so a message that arrives mid-run belongs to the next task and is never absorbed. - Auto-retry inherits the root input owner and is queued at a bumped priority, created inside FailTask's transaction so no newer chat task can jump ahead. - CompleteTask writes exactly one assistant outcome inside the completion transaction: a normal message, or a visible no_response outcome (with a non-empty English fallback) when the final output is empty. The write failing rolls the completion back and the handler returns 5xx so the daemon retries; the status CAS keeps it idempotent. chat:done carries message_kind. - Web/desktop/mobile render no_response as a localized 'no text reply' state (keeping the tool timeline), suppress Copy, keep it unread, and keep the session-list preview non-blank. - Legacy/channel tasks (chat_input_task_id NULL) keep the trailing-message selector, so a rolling deploy never replays Slack/Lark history. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): scope no_response to direct tasks; don't cancel task on input read error (MUL-4351) Addresses PR review (Niko): - writeChatCompletionOutcome only writes a no_response row for task-owned direct tasks (chat_input_task_id set). Legacy/channel (Slack/Lark) tasks keep the prior behavior: empty output writes no assistant row, so chat:done carries empty content and the channel outbound silently drops it — the no_response fallback body never reaches an external channel. - The daemon claim distinguishes a genuine zero-input batch from a failed input read: on ListChatInputMessages / ListChatMessages error it returns 5xx and preserves the dispatched task for redelivery instead of cancelling a valid task on a transient DB error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6c3b79db19 |
feat(daemon): bound daemon.log size with rotation (MUL-4330) (#5170)
* feat(daemon): bound daemon.log size with rotation (MUL-4330) The background daemon redirected its stdout/stderr into daemon.log opened O_APPEND and never rotated it, so the file grew without limit until it was too large to open. Every structured log line already flows through slog (including agent subprocess stderr, forwarded via newLogWriter), so the daemon's logger is effectively the sole author of the file's volume. Route the foreground daemon's slog output — both the injected component logger and the package-global slog default — through a size-based rotating writer (lumberjack) that keeps the active daemon.log small (20MB default, 5 gzip-compressed backups, 30d), all env-overridable. Raw crash output (Go runtime panics, pre-logger errors) now goes to a separate daemon.err.log so the child's inherited fds never hold daemon.log open, which would block rotation's rename on Windows. The Desktop app spawns the daemon via this same launcher and its log tail already handles size-shrink, so both CLI and Desktop are covered. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): address log-rotation review — foreground output, Windows handles, bounded err log (MUL-4330) Resolves the blocking review items on the daemon.log rotation change: 1. Windows first-upgrade rotation: a foreground managed daemon now re-points its own stdout/stderr to daemon.err.log at startup (SetStdHandle) before building the rotator, releasing any daemon.log handle an older self-update launcher inherited (Go opens files without FILE_SHARE_DELETE, which would otherwise block rename-on-rotate). No-op on Unix, where an open fd never blocks rename. 2. `daemon logs -f` vs rotation: Unix uses `tail -F` (reopen by name); Windows opens the reader with FILE_SHARE_DELETE so it can't block the rotator's rename, and reopens the file on size-shrink to follow across rotation. 3. Self-update handoff no longer briefly runs two rotators on one file: the old process closes its rotator and moves remaining handoff logs (incl. the slog default) to the crash sink before the successor starts. 4. daemon.err.log is now bounded: it rolls to a single ".1" backup once past 5MB at open time, so a crash loop can't move the growth problem to it. It is also surfaced in the troubleshooting docs. 5. Explicit `--foreground` in a terminal keeps live stdout/stderr logging (a documented debugging path); only detached/background children rotate into daemon.log. Decided by whether stderr is a terminal. Also: rotation env knobs now reject 0/negative (0 means 100MB / keep-all in lumberjack), preventing an accidental unbounded config. Adds unit tests for the err-log rolling and positive-int parsing; Windows/Linux(arm64) cross-builds and `GOOS=windows go vet` pass. Co-authored-by: multica-agent <github@multica.ai> * docs: sync zh/ja/ko troubleshooting with daemon.log rotation + daemon.err.log (MUL-4330) Co-authored-by: multica-agent <github@multica.ai> * test(handler): bump the agent's own runtime version in quick-create parent test (MUL-4330) TestQuickCreateIssueParentTrustBoundary bumped an arbitrary `LIMIT 1` agent_runtime, but the handler version-checks agent.RuntimeID — the runtime bound to the request's agent. In the shared handler test workspace, other tests register additional runtimes, so the two diverge and the agent's real runtime keeps the seed's empty cli_version, tripping the daemon-version gate (422 daemon_version_unsupported) before the parent_issue_id assertions run. Bump the runtime tied to the agent instead, making the setup deterministic. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fe46dfdbf6 |
MUL-4203: Fix Cursor MCP auth source seeding (ZIC-52)
Merge approved PR. |
||
|
|
01f28e8af6 |
docs(changelog): add v0.3.42 release entry across en/zh/ja/ko (#5159)
Adds the daily v0.3.42 changelog entry to all four localized landing sites: a dedicated Chat tab, LLM-generated chat titles, cancelled issues as a first-class column, agent model/effort on hover, plus reconnect/comment-delivery/agent-mention/link/version fixes. Typecheck and the changelog unit test pass locally. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a51ab4d551 |
feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) (#5076)
* feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) Replace the floating chat FAB/window with a first-class Chat tab under Inbox, laid out as an IM-style two-pane surface (thread list + conversation). Highlights: - New Chat page (packages/views/chat/chat-page.tsx) with URL-addressable session selection; web + desktop routing wired up. Removes the old chat-fab / chat-window / resize-handles / context-items paths. - IM thread list: agent avatar + last-message preview + IM timestamp, red unread *count* badge (read-cursor model), presence-gated typing vs waiting. Rename lives only in the conversation header ⋯ menu (not the list hover). - Per-session conversation header (rename / view agent / delete), agent-aware empty state (avatar + name + description + starter prompts), and a deterministic clean-title derivation from the first message. - Server: read-cursor unread model (migration 145) and per-user pinned agents (migration 146, dedicated chat_pinned_agent table + handler/queries). New-agent welcome chat auto-enqueues a real agent run (LLM intro, no static template). - Design: fade the global --border token; borderless list headers on Chat/Inbox, kept (faded) on the conversation header. Verified: pnpm typecheck (all packages), go build ./..., go vet, gofmt. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): make new-agent welcome read as an agent-initiated intro (MUL-4230) The "meet your new agent" chat used to insert a fake user message ("👋 Hi! Please introduce yourself …") and have the agent reply to it, so the thread looked like the creator prompting the agent. Drop the persisted user message. Flag the auto-created session is_agent_intro (migration 147) and drive the intro run server-side: the daemon builds a proactive self-introduction prompt for such sessions (buildChatPrompt) instead of a "reply to their message" prompt. The intro stays LLM-generated; the thread now opens with the agent's own message, as if it reached out first. - migration 147: chat_session.is_agent_intro - CreateChatSession carries the flag; sendAgentWelcomeChat no longer persists/publishes a user message - daemon: ChatIntro threaded from session flag → intro prompt Co-authored-by: multica-agent <github@multica.ai> * feat(chat): Settings toggle for the floating chat window (MUL-4235) (#5080) * feat(chat): Settings toggle for the floating chat window (MUL-4235) Re-introduce the floating chat overlay on top of Chat V2 as an optional, Settings-gated surface instead of deleting it outright. - Settings → Preferences → Chat: a switch (floatingChatEnabled, persisted client preference, default ON) to show/hide the floating window. - FloatingChat wrapper owns the two gates: the preference, and the /chat route (hidden on the tab so the same activeSessionId isn't shown twice). - ChatFab + a compact ChatWindow that reuse the shared useChatController and conversation components, so activeSessionId stays in lockstep with the tab. - Restore use-chat-context-items so the overlay's @ surfaces the current issue/project (the 'current context' affordance) — the tab stays manual. - i18n (en/zh-Hans/ja/ko), store unit tests. typecheck: core/views/web/desktop green. tests: chat store 9, settings 82, chat 39 pass. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): dedicated Chat settings tab, floating window opt-in (MUL-4235) Address review: give Chat its own Settings tab instead of a section inside Preferences, and default the floating window OFF (opt-in). - New Settings → Chat tab (chat-tab.tsx) under My Account; moves the floating-window toggle out of the Preferences tab. - floatingChatEnabled now defaults OFF — only an explicit enable from the Chat tab mounts the FAB/overlay. - i18n: page.tabs.chat + a top-level chat block (en/zh-Hans/ja/ko); revert the Preferences chat section and its test mock; store tests updated for the opt-in default. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop starter prompts from chat empty state (MUL-4237) (#5081) The three starter prompts (List my open tasks by priority / Summarize what I did today / Plan what to work on next) read as filler more than help, so remove them along with the now-unused returning_subtitle ("Try asking"). The empty state keeps its agent-aware header — avatar + "Chat with {name}" + optional description — and the composer stays the entry point. Locale keys dropped across en/zh-Hans/ja/ko (parity preserved). Based on the Chat V2 branch (parent MUL-4171, #5076), not main. Co-authored-by: Lambda <lambda@multica.ai> * feat(chat): pin a chat to the top of the Chat list (MUL-4240) (#5082) Builds on Chat V2 (#5076). Adds a per-conversation pin so a user can keep important chats at the top of the IM-style thread list, above the activity-sorted rest. Backend: - migration 148: chat_session.pinned_at (nullable) + partial index; the timestamp doubles as the pinned-group sort key and the boolean flag. - list queries order pinned-first, then by most-recent activity. - SetChatSessionPinned query + PATCH /api/chat/sessions/{id}/pin handler; pinning never bumps updated_at, so an unpinned chat won't jump the list. - ChatSessionResponse.pinned + chat:session_updated carries the new state. Frontend: - ChatSession.pinned; setChatSessionPinned API + useSetChatSessionPinned with optimistic re-sort; shared sortChatSessions comparator. - thread list: pin indicator on pinned rows + pin/unpin hover action; list sorted pinned-first so it stays ordered after cache patches. - realtime patch re-sorts on pin change; en/ja/ko/zh-Hans strings. Tests: SetChatSessionPinned handler test, sortChatSessions unit tests. * feat(chat): round send button, move file upload into a + menu (MUL-4250) (#5088) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): match chat list selected-item style (inset padding + rounded) (#5093) Wrap the inbox list in p-1 and give each row rounded-md/px-3 so the selected bg-accent reads as an inset rounded card — same treatment the chat thread list already uses — instead of a full-bleed, sharp-cornered highlight. Content stays 16px-inset (p-1 + px-3 == old px-4). MUL-4253 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): rename + menu upload item to "Image or files" (MUL-4250) (#5092) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop welcome intro session repeating the same introduction (MUL-4259) The is_agent_intro flag on chat_session is persistent, so every follow-up turn on a welcome session re-selected the self-introduction prompt in buildChatPrompt and the agent kept replying with the same intro instead of answering the user. Gate resp.ChatIntro at claim time on the session still having zero human (role='user') messages via a new ChatSessionHasUserMessage query: the first, message-less server-driven turn introduces the agent; once the creator replies, later turns fall back to the normal reply prompt. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review findings + unbreak CI (MUL-4171) - task:failed now refreshes the sessions list (invalidateSessionLists), so the thread-list preview / unread / sort stays correct after an agent failure — FailTask persists a failure chat_message but only broadcasts task:failed, mirroring the chat:done success path. - Self-heal stale chat deep links: once the sessions list has loaded and a ?session= id isn't in it (deleted / no access / never existed) with nothing in flight, clear the selection instead of rendering an editable empty chat that would POST into a nonexistent session. Freshly-created sessions are exempt (they carry optimistic messages + a pending task). - CI: add the new parameterless `chat` route to link-handler's WORKSPACE_ROUTE_SEGMENTS and to paths/consistency.test.ts (route set + expectedSegments) — keeps the two in sync, fixes the failing @multica/core test. - Fix a MUL-4235/MUL-4237 merge collision that broke @multica/views typecheck: chat-window.tsx still passed the removed `onPickPrompt` prop to EmptyState. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): self-heal dangling session in shared controller, not just ChatPage (MUL-4171) Re-review follow-up: the stale-session self-heal only lived in ChatPage, so the floating ChatWindow still entered from a persisted activeSessionId and would render an editable empty chat (then POST into a nonexistent session) when the selected session was deleted / lost access off the /chat route. - Move the self-heal into the shared useChatController so every surface (tab and floating window) drops a dangling activeSessionId once the sessions list has loaded and doesn't contain it. - Harden ensureSession: trust the current id only when it's in the loaded list or is a just-created session still awaiting the refetch; a dangling id falls through to create a fresh session instead of POSTing into a 404. - Exempt just-created sessions via an OPTIMISTIC-write signal (hasOptimisticInFlight: pending task or optimistic- message), not hasMessages — a session deleted elsewhere with real cached history stays eligible for self-heal. Add a unit test for the discriminator. Co-authored-by: multica-agent <github@multica.ai> * test(views): fix app-sidebar useWorkspacePaths mock for the new chat nav (MUL-4171) The AppSidebar personal nav gained a `chat` item, so it calls `useWorkspacePaths().chat()` at render. The app-sidebar.test.tsx mock hadn't been updated, so `p.chat` was undefined and every render threw `TypeError: p[item.key] is not a function`, failing @multica/views#test in CI. - Add `chat: () => "/acme/chat"` to the mocked useWorkspacePaths. - Route the chat-sessions query key through a mutable `chatSessions` fixture. - Add coverage for the Chat nav: renders the link, badges the summed unread_count, and hides the badge when all sessions are read — so this drift is caught next time. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): use the original floating window, not the rewritten one (MUL-4235) (#5102) Follow-up to the merged #5080, which shipped a hand-written, simplified ChatWindow and lost the original's animations / drag-resize / expand-minimize. The floating window is just a quick entry point — it should be the original UI, not a rewrite. - Restore chat-window.tsx, chat-fab.tsx, chat-resize-handles.tsx and use-chat-resize.ts verbatim from main (0-diff): motion animations, drag resize, expand/minimize and the session dropdown are back. - Restore the empty_state.returning_subtitle + starter_prompts i18n keys the original window renders (V2 had dropped them); drop the now-unused window.open_full_tooltip key the rewrite added. - Settings gating is unchanged: FloatingChat still wraps the original FAB + window, gated by floatingChatEnabled (default off) and hidden on /chat. typecheck: core/views/web/desktop green. tests: chat + settings views 126 pass. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): archive chats from the list, delete only from Archived (MUL-4263) (#5098) Restore an archive flow as the reversible sibling of delete: - Chat list hover now offers Archive (not Delete); pin/stop unchanged. - A footer entry ('Archived · N') opens an Archived view listing archived chats; hard delete lives only there (hover -> unarchive + delete, with the existing inline confirm). - Conversation header ⋯ menu mirrors this: active chats archive, archived chats unarchive/delete. Backend: PATCH /api/chat/sessions/{id}/archive flips status active<->archived (SetChatSessionArchived), broadcasts status on chat:session_updated so other tabs re-sort into the right list. SendChatMessage already refuses archived sessions, so archived chats stay read-only until unarchived. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) (#5100) * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop chat-list archive marker, keep conversation read-only (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * feat(chat): apply archived-agent read-only to the floating chat window (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address floating-window + archived-agent review blockers (MUL-4171) Re-review follow-up on the restored floating ChatWindow + archive flow: 1. Floating stale-session self-heal. The restored ChatWindow doesn't use the shared controller, so its ensureSession trusted any non-empty activeSessionId and there was no dangling-session cleanup — a deleted / no-access persisted session could send into a nonexistent session. Ported the same guard used for the tab: a self-heal effect that clears a dangling activeSessionId once the sessions list has loaded, and ensureSession only trusts an id that's in the list or has an in-flight optimistic write (hasOptimisticInFlight, reused from use-chat-controller). handleSend seeds the optimistic message + pending task before setActiveSession, so a freshly-created session is never mis-cleared. 2. Floating dropdown bypassed archive-first safety. Its active rows offered a hard-delete, letting the floating window destroy active chats and skip the "archive first, delete only from Archived" model. Active rows now ARCHIVE (reversible, one-click) like ChatThreadList; the floating window offers no hard-delete — unarchive/delete live only in the full Chat page's Archived view (reachable via expand). Removed the now-dead delete-confirm machinery. 3. Orphan user message on archived-agent send. SendChatMessage created the chat_message before EnqueueChatTask, which rejects an archived / runtime-less agent — a stale client would land a user message then get a 500, orphaning it. Added a preflight that checks the session agent's archived / runtime state and returns 409 before any mutation, plus a handler test asserting the send is rejected with no message persisted. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |