mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
agent/lambda/llm-env-plumbing
431 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0314df3b8e |
docs(license): widen the branding condition to all UI code and add NOTICE (MUL-5558) (#6197)
* docs(license): widen the branding condition to all UI code and add NOTICE (MUL-5558) The branding condition in 1b defined Multica's "frontend" as `apps/web/` only, which left the code that actually renders the console brand outside its own scope: the sidebar, invite and new-workspace brand surfaces live in `packages/views/`, and the Electron and iOS clients mount the whole console from `@multica/views` without touching `apps/web/`. A rebranded desktop build could therefore satisfy the condition literally while removing every Multica mark. Replace the directory-enumerated "frontend" with a derivation-based "Multica user interface" covering `apps/web/`, `apps/desktop/`, `apps/mobile/`, `packages/views/` and `packages/ui/` across source, the Docker "web" image, and compiled desktop/mobile binaries. Keep the non-interface exemption so backend-only use stays governed by 1a rather than by branding, and add 1c so that path still carries attribution. Add a NOTICE file to give the attribution obligation somewhere to land: the repository had none, and no per-file copyright headers, so Apache 2.0 section 4(c)/(d) had nothing to reproduce. Move the "commercial license must be obtained" sentence into 1a, since it describes only that condition and 1b/1c are cured by written authorization and attribution respectively, not by purchase. Co-authored-by: multica-agent <github@multica.ai> * docs(license): separate the commercial license from the branding waiver (MUL-5558) Conditions (a) and (b) were both cured by "explicitly authorized by Multica in writing", so one authorization letter could be read as releasing both — handing over commercial hosting rights when only a rebranding permission was intended. Name the two grants distinctly: (a) is cured only by a commercial license obtained from the producer, (b) only by a written branding waiver. Add (d) stating that neither implies the other and that neither can be inferred from the producer's silence or from acceptance of contributions. Co-authored-by: multica-agent <github@multica.ai> * docs(license): name the published frontend image and cover relocated UI code (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * chore(docker): ship LICENSE and NOTICE in the published images (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * docs(readme): add bilingual License sections matching the updated conditions (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * docs(license): reposition as the self-contained, source-available Multica License (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * chore(release): align license metadata and ship NOTICE in release artifacts (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * docs(readme): describe Multica as source-available and add contribution terms (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * fix(landing): describe Multica as source-available instead of open source (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> * revert(license): keep the Dify-style open-source framing, scope widening only (MUL-5558) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9c90327ce4 |
fix(ui): replace the text-transparency ladder with solid tones (MUL-5452) (#6152)
* fix(ui): replace the text-transparency ladder with solid tones (MUL-5452) Hierarchy was being expressed with transparency: 152 call sites of text-muted-foreground/30..80, 26 of text-foreground/60..90, plus a handful on destructive and current, and a few written as a standalone opacity-* utility instead of a slash alpha. On light surfaces every muted variant failed WCAG AA - /80 reached only 3.78:1 and /40 sat at 1.80:1, below even the 3:1 floor for non-text - because the palette had no step below --muted-foreground, so transparency was the only tool for 'quieter than muted'. The palette now has that step, and it is deliberately non-text: --faint-foreground clears 3:1 (WCAG 1.4.11) on every surface for icons, chevrons, separator glyphs and empty-cell em dashes. There is no room for a third readable text tone - AA caps a lighter text tone 0.018 L away from muted - so text keeps exactly one floor, --muted-foreground. Also fixes text-destructive/70 on a cron error message, which was 3.61:1. This branch changes zero font sizes. The sub-12px half of the issue is MUL-5451's (#6136); keeping the two apart is what makes this one reviewable on its own after #6108 was reverted. apps/web/app/text-contrast.test.ts replaces muted-foreground-contrast.test.ts rather than sitting beside it. It recomputes the floors from tokens.css instead of hard-coding ratios, and fails the build on all four ways to spell the defect: /70, /[0.5], /[50%], and a detached opacity-* in the same class string. Transparency behind hover/focus/disabled stays allowed - the resting state carries the contrast obligation and it is solid. Co-authored-by: multica-agent <github@multica.ai> * fix(ui): correlate transparency across a whole class expression Review found two ways past the guard, both real. A per-literal check cannot see cn("… text-muted-foreground", suppressed && "opacity-60") - one element wearing a colour in one argument and a dim in the next. That split shape is the common one, and it was hiding live violations: the comment trigger chips dimmed aria-pressed label text to 2.55:1 while the sweep reported clean. The second was my own exemption. Accepting any state word within 80 characters let "text-muted-foreground hover:text-foreground opacity-50" through, because the hover: belongs to the colour, not to the opacity. The detector now correlates across a whole cn() call or template literal, splits it into segments that each carry their own condition, and exempts only on the variant prefix the opacity utility itself carries or on the condition governing its segment. Segment splitting is what keeps ${disabled ? "opacity-60" : ""} exempt while flagging its neighbours. Fixed what that surfaced: three trigger-chip controls (the suppressed state is already carried by the avatar's own grayscale, the sentence wording and a solid muted step), a disabled-skill icon chip, and the diff gutter marker. tab-bar's isDragging is exempt - a drag ghost is an in-flight gesture, the same category as :active. The detector now has its own table of thirteen cases. Every hole so far has been silent, so the shapes it must and must not catch are pinned next to the reason each one exists. Co-authored-by: multica-agent <github@multica.ai> * test(ui): cover the faint tone in the cn() merge regression test text-faint-foreground is a new text-<x> class, which is the exact shape that silently broke the sidebar labels in #6108: tailwind-merge cannot tell a size from a colour and drops one of them. The token does resolve correctly today - verified both orders against a size step and against another colour - but the test that exists to catch this was not covering it, so the guarantee rested on nothing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
577018649e |
feat(issues): support human-readable issue URLs using issue keys (MUL-5354) (#6117)
* feat(issues): support human-readable issue URLs using issue keys (MUL-5354) Closes #5987. `/{ws}/issues/MUL-123` now opens the issue, the copy-link action shares that form, and a UUID URL rewrites itself to it. Existing UUID links keep working. Backend already resolved identifiers on `GET /api/issues/{id}`, but it compared the number only — every prefix with the right number opened the same issue, so no identifier URL could be canonical. Resolution now validates the prefix against the workspace's own (case-insensitively, matching `lookupIssueByIdentifier`), and the number parser bails on int32 overflow instead of truncating a digits-only UUID group into a plausible issue number. On the client the identifier stays a presentation concern: the route resolves it to the UUID before rendering, because the realtime updaters patch `issueKeys.detail(wsId, issue.id)` with the UUID from the websocket payload. A view keyed on the identifier would sit on a cache entry no realtime event can reach and silently stop updating. Resolution reuses the request the detail view would have made anyway and seeds the UUID-keyed entry, so an identifier URL costs no extra round trip. The desktop tab title/status glyph hops through the same resolution for the same reason. The URL rewrite lives in the new route wrapper rather than IssueDetail: the inbox renders IssueDetail in a side panel, where replacing the URL would navigate the user out of the inbox. No migration — `issue (workspace_id, number)` is already unique/indexed. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): make the single-request guarantee for identifier URLs explicit Review flagged that opening `/{ws}/issues/MUL-123` fires two detail requests. It does not, under the app's own QueryClient — but the guarantee was resting on something implicit, so make it structural. The old shape seeded the UUID-keyed entry from a `useEffect` after resolution, while the route enabled the UUID query in the same render. That held only because the seed effect happened to be declared before the UUID query's own effect, and because `createQueryClient` sets `staleTime: Infinity` so a seeded entry is never refetched. Neither is obvious from the code, and a diagnostic run under a bare `new QueryClient()` (staleTime 0) does show two calls — the second being a staleness refetch of an already-seeded entry, i.e. a harness artifact. `useCanonicalIssueId` becomes `useCanonicalIssue`, which owns both the resolution query and the canonical detail query and hands the resolution response to the latter as `initialData`. That is applied while the observer is created, so the canonical query never observes an empty cache and never starts a fetch of its own — no dependency on effect ordering, and no cache write that could race a realtime patch (`initialData` is ignored once the entry holds data). Callers collapse to one hook each: the route no longer runs its own detail query, and the desktop page drops its duplicate. Tests now build the client with `createQueryClient()` rather than a bare `new QueryClient()`, so request-count assertions measure production behavior instead of the harness, plus a direct assertion that an identifier URL costs exactly one request. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): stop the request loop when an identifier names no issue Opening `/{ws}/issues/ZZZ-134` never reached "not found". It spun an unbounded request loop and left the UI on the loading skeleton forever. The route treated a failed resolution as "nothing resolved" and handed the raw identifier down to IssueDetail. IssueDetail mounted a second observer on the query that had just failed; `retryOnMount` refetched it, which flipped the resolve hook back to pending, which unmounted IssueDetail, which remounted it when the refetch failed — and around again. Measured with retry disabled to isolate it: 8,192 requests at 300ms, 32,768 at 600ms. Under the app's `retry: 1` the backoff only paces the loop, it still never converges. `useCanonicalIssue` now reports a terminal `notFound` read from the resolution query's own error state, rather than leaving callers to infer failure from "not resolving and no id" — an inference that cannot distinguish failed from in-flight. `IssueDetailRoute` renders the not-found UI itself and never hands an unresolved segment to a view that would query it again, so no second observer exists to restart the cycle. Same measurement after the fix: 1 request, settled, "not found" on screen. The not-found UI moves out of IssueDetail into a shared `IssueNotFound` so both render the identical state. Regression tests at both levels, with retry off so any count above 1 can only be a remount refetch: the hook settles a failed resolution without looping, and the real IssueDetailRoute holds at one request across waits and rerenders. Both fail against the previous code. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7803a5b9ea |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6136)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 written as arbitrary values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the ~1,900-call-site rename moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: 8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30 (text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small buttons and toggles. Half-pixel sizes are gone. This supersedes #6108, which was reverted by #6116 because the sidebar group labels rendered at the inherited 16px. The cause was not the scale but cn(): `text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a table listing only the default sizes, so it filed every role step under text-colour and dropped whichever of `text-caption` / `text-sidebar-foreground/70` came first. Registering the steps as a font-size class group restores the real conflict groups — size beats size, colour beats colour, the two coexist — and a test pins the list against the scale, since the failure is silent in source. Hand-written CSS is covered too. The transcript kept a 12.5px body long after every Tailwind call site was on the scale, so the "no half-pixel sizes" claim was true of the classes and false of the product; the editor's prose, code and mermaid ramps had the same blind spot, and seven of their eight values already equalled a step exactly. All now reference var(--text-*). The guard test reads raw `font-size:` declarations as well as class names, exempting only the 16px iOS input-zoom workaround in base.css and the landing pages' marketing ramp. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing display type (rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative emoji / serif-hero sizes. Verified on a running local stack: pinned sidebar rows and group labels measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit of every rendered font size across the product surfaces finds nothing off the scale; the only exceptions are avatar initials and emoji, which actor-avatar.tsx sizes proportionally to the avatar diameter by design. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
545afea827 |
Revert "feat(ui): establish a role-named type scale and migrate ad-hoc font s…" (#6116)
This reverts commit
|
||
|
|
d68d636c91 |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6108)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 of them written as arbitrary text-[Npx] values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the 1,900-call-site rename (text-sm -> text-body and friends) moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: - 8 / 9 / 10px -> micro (11px): 102 sites, mostly badges and overline labels. Deliberate — under 12px is a counter role, not a text role. - 17px -> title (18px), 22px -> display-sm (24px), 30px (text-3xl) -> display (36px): 21 sites, all headings or stat numbers in flexible containers. - Half-pixel steps are gone entirely. Arbitrary sizes also inherited whatever line-height was above them; the tokens now pin one, which removes latent overflow risk in the fixed-height h-4/h-5 badges those sizes were used in. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing-page display type (rem/clamp, 2.2-6.4rem) is marketing typography on a separate ramp and stays out of the scale, as do four decorative emoji / serif-hero sizes. A guard test fails the build on any font size written outside the scale, reporting file:line — without it nothing in a Tailwind build makes an off-scale value look wrong, which is how the drift happened. Verified against a local stack: typecheck, lint and the full TS suite show no new failures, and before/after screenshots of issues, issue detail, runtimes (populated), usage, agents, inbox, my-issues and settings differ only in the intended 10px -> 11px labels, with no row-height, truncation or layout shift. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4f70480039 |
fix(typography): real Inter italic + variable Geist Mono on desktop (MUL-5449) (#6105)
* fix(typography): load real Inter italic on web and desktop (MUL-5449) Neither platform loaded an Inter italic face: next/font defaults to `style: ["normal"]`, and desktop imported `@fontsource-variable/inter` without `wght-italic.css`. Every `italic` in the product was therefore browser-synthesized oblique — the ~20 semantic UI labels (chat empty states, model-picker's "Managed by runtime", dashboard/squad placeholders) plus all markdown <em> and blockquotes, which are user content. Load the real face on both, mirroring how Source Serif 4 is already wired. Also set `font-synthesis: style` on html, which forbids weight synthesis while leaving style synthesis on. Weight synthesis is pure loss now that every loaded face has real weights, and it is actively harmful on the CJK tail of --font-sans: `font-bold` against PingFang SC (which stops at Semibold) makes Chromium smear a fake 700 that closes up the counters of dense Han glyphs. Style synthesis stays on deliberately. `auto` only synthesizes when no real italic matches, so the newly loaded Inter Italic already wins for Latin. The only stacks still synthesizing are the two that ship no italic at all — CJK fallbacks and Geist Mono — where `font-style: italic` has no other visual carrier; a blanket `font-synthesis: none` would silently flatten every Chinese <em>, every editor italic mark and every hljs comment to upright. Co-authored-by: multica-agent <github@multica.ai> * fix(typography): give desktop the variable Geist Mono (MUL-5449) Web gets a variable Geist Mono from next/font (`font-weight: 100 900`, verified in the emitted CSS), but desktop loaded only the discrete 400 and 700 cuts. Any weight in between silently snapped to the nearest one desktop had, so the same shared component rendered at two different weights on the two platforms: - `font-mono font-medium` (500) fell back to 400 in chart.tsx, webhook-event-filter-section.tsx and keyboard-shortcuts-tab.tsx. - Inline <code> in rendered markdown has no explicit weight, so inside a <strong>, heading or <th> — all `font-semibold` — it inherits 600 and resolved up to 700. Loading `@fontsource/geist-mono/500.css` would have fixed only the first of those. Switching to `@fontsource-variable/geist-mono` covers 100-900 in one file and closes the whole class, matching how Inter and Source Serif 4 are already wired on desktop. The latin subset is also smaller than the two static cuts it replaces: 22.6 KB vs 14.4 + 14.9 KB. Co-authored-by: multica-agent <github@multica.ai> * docs(typography): correct stale Geist Mono note in font-synthesis comment The comment was written when desktop still loaded discrete 400/700 cuts. The following commit swapped desktop to @fontsource-variable/geist-mono, so Geist Mono is now variable on both platforms and the "ships discrete cuts" clause was false. Also name landing's 400-only Instrument Serif so the "every face we load has real weights" claim is complete. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
420ffe7dbc |
feat(diagnostics): capture the JS stack of a hung renderer (MUL-5345) (#6026)
* feat(diagnostics): capture the JS stack of a hung renderer (MUL-5345) Route attribution shipped in 0.4.12 and did its job: hard hangs are no longer scattered, they cluster on one page (10 of 12 hangs, 8 distinct users, spread over 16 hours). It also hit its ceiling there. That page hosts three modes on a single route and the mode lives in component state, so the route field cannot say which of them froze — and a page name was never going to name the function either. Two rounds of code-reading produced two hypotheses and both were wrong, and one manual repro attempt covered a path the telemetry never pointed at. Naming the code requires reading it off the stuck thread. When the renderer hangs, the main process attaches the DevTools protocol and asks for the stack. The channel is warmed while the renderer is healthy because a command sent after the thread is stuck is never dispatched — measured on the pinned Electron 39.8.7, where a post-hang attach returned nothing in 5s while a pause on a warm channel returned the stack in 2ms with the blocking function on top. Holding the channel open all session showed no cost beyond run-to-run noise (A/B/A; the ordering drift between cold phases exceeded the effect). That channel is the reason this ships behind a fail-closed server flag rather than on by default. `desktop_hang_stack_capture` rides the existing `/api/config` feature flags; main starts off, only an explicit `true` enables it, and revoking it detaches the channels rather than merely skipping the next capture. Main cannot read config itself, so the renderer forwards the one bit — which means a config that never arrives also lands on off. Privacy is unchanged in kind from `$exception`: four scalar fields per frame, `scopeChain` and `this` dropped so no handle can be dereferenced into user data, script URLs reduced to their bundle-relative tail, and a four-verb CDP allowlist that a source-level test pins to a single callsite. Resume is unconditional — a capture must never turn a recoverable hang into a permanent one. Delivery is fixed alongside, because a stack that cannot be sent is not worth capturing: `freeze:get-last` no longer deletes on read, the report goes out with `send_instantly`, and the breadcrumb is retired only after a grace window, so a second hang inside that window leaves the file for the next boot instead of taking the report with it (the MUL-4115 failure mode). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): close the kill switch, egress and multi-window gaps (MUL-5345) Three review findings on #6026, all in failure paths the tests didn't reach. The kill switch didn't reliably revoke. `coolDebuggerChannel` only detached after `Debugger.disable` resolved, so a failed disable left the channel attached — the exact state the switch exists to exit. Disable is a courtesy to the renderer; detach is the contract, so it now runs in `finally`. Warming had the mirror bug: an attach we made and could not enable returned false while leaving the channel open, stranding a debugger on a renderer nothing tracks. That attach is rolled back now, and only that one — a channel someone else owns (DevTools) is left alone. Stack frames were sanitized at capture and then forwarded verbatim at egress. Between those two points they cross an on-disk breadcrumb that `readFreezeBreadcrumb` barely validates, by design: it only has to survive version skew. So "sanitized once" was not a property the flush side could rely on — an older build, a corrupt file or a future writer could put a `scopeChain` handle or an absolute install path in there and it would ship. Both ends now rebuild frames through one shared whitelist, which also makes them impossible to drift apart. The url reduction is idempotent so re-running it costs nothing. The control flag was global, and that does not survive multiple windows. Every renderer publishes `false` before its own config lands, so a window opened while capture was on either never warmed (the global value never changed, so nothing warmed the new webContents) or cooled every other window on its way up. State is per renderer now; they converge on the same value because they read the same config, but each on its own schedule. Regression tests for each: detach after a throwing disable and rollback after a throwing enable, a frame carrying `scopeChain` / `this` / an absolute path reaching the flush side, and a second window warming while the first is already on without revoking it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ceba14a227 |
fix(issues): MUL-5362 return to the source list after deleting an issue (#5997)
* fix(issues): return to the source list after deleting an issue Deleting an issue from its detail page always pushed the workspace Issues list, so opening an issue from My Issues (or a project list, search, a pin, an agent panel) and deleting it dropped the user's navigation context — GH #5995. Go back instead. `useBackOrReplace` steps back when the platform reports in-app history and replaces with a fallback path when there is none, so a shared link opened cold or a new tab never steps off the app. Web answers via the Navigation API, falling back to counting its own pushes; desktop reads the active tab's virtual history. `replace`, not `push`: the deleted issue's URL must not stay in history for the back button to land on a 404. The not-found "Back to Issues" button loses the same context, so it moves to the same helper and its label becomes a plain "Back". Co-authored-by: multica-agent <github@multica.ai> * fix(web): track history position, not push count, for canGoBack The Navigation API fallback only ever counted pushes, so `pushes > 0` did not mean the current entry still had an in-app page behind it. Cold-open an issue, click Issues, press the browser's Back button, then delete: the tracker still claimed history and `back()` would step off Multica — the exact case the fallback exists to prevent. Count depth instead: a push adds one, any traversal takes one away (clamped at zero). Reaching the document's first entry requires traversing back at least as many times as we pushed, so a positive depth can never be claimed while sitting on it. A browser Forward is now conservative — it reports no history where a step back would have been fine — which costs a fallback navigation rather than an exit. Also corrects the useBackOrReplace contract comment: stepping back leaves the dead URL in forward history, so the guarantee is that it never lands on the back stack, not that it leaves history entirely. Co-authored-by: multica-agent <github@multica.ai> * fix(web): answer canGoBack from the browser alone, never from push counts Review found the counter still lied, one level deeper: it counted `router.push` calls, and a call is not a committed history entry. Next drops a push to `replaceState` when the canonical URL is unchanged (app-router.js, the `pendingPush && href !== canonicalUrl` branch), so clicking a self-link — the breadcrumb on an issue detail page, a pin to the issue you are on — incremented depth with no entry behind it, and a delete from there could still step off the app. Fixing the count needs per-entry markers, which means depending on both React effect ordering (our provider's effect runs before app-router's history commit) and Next's own preserveCustomHistoryState behaviour. Two rounds of review have now found holes in hand-rolled history tracking; a third layer of it is not the way to buy this guarantee. So stop deriving. `canGoBack` is the Navigation API's answer or `false`. Browsers without it take the fallback path, which is exactly what they did before any of this existed — nobody regresses, and the "wrong true walks the user out of the app" failure is now unreachable by construction. Drops the tracker, the popstate wiring and the push wrapper: the `multica:navigate` bridge returns to its original shape. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
38b08acf00 |
feat(diagnostics): report which page a desktop hang happened on (MUL-5345) (#5989)
* feat(diagnostics): attribute desktop hangs to route, function and stack (MUL-5345) A desktop hang currently reports "froze for 8s" and nothing else, so MUL-5345 could not be diagnosed at all. Three gaps, all fixed here. Route attribution was silently dead. The main window's route reporting lived in the PostHog pageview tracker and was deleted with it (MUL-4127), leaving `getDiagnosticContext` in main reading a WeakMap nothing ever wrote — every field report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push, and now feeds the in-renderer watchdog too: the renderer runs a memory router, so `location.pathname` could never identify the page either. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing. Function attribution did not exist. The watchdog now prefers `long-animation-frame` over `longtask` where supported, which carries per-script `sourceFunctionName` / `sourceURL` / `sourceCharPosition`. That covers hangs the thread survives. For hangs it does not, main captures the JS call stack over CDP — which requires the Debugger channel to be warmed at window creation, because a command sent after the thread is stuck never gets dispatched. Commands go through a four-verb allowlist, only scalar code locations are copied out of the paused frames (never `scopeChain`, whose handles dereference into user data), and resume is unconditional so a capture can never turn a recoverable hang into a permanent one. Reports could also be lost before delivery. `freeze:get-last` no longer deletes; the renderer sends with `send_instantly` and acks by exact timestamp, so a report killed by a second hang is retried next boot instead of vanishing with the file (the MUL-4115 failure mode: three deterministic hangs, zero events). A 7-day TTL keeps an undeliverable breadcrumb from becoming permanent boot noise. Operations name themselves: `parseMarkdownChunked` marks the diagnostic context before it runs, and the mark travels to main over the async IPC channel that still lands after the main thread stops responding. The event carries how stale the mark is, so it reads as context rather than as a cause. Verified on Electron 39.8.7 / Chromium 142 (throwaway spike, not committed): Debugger.pause during a 12s synchronous block returned the stack in 2ms with the blocking function on top; holding the channel open all session showed no cost beyond run-to-run noise (A/B/A, ordering drift larger than the effect); DevTools and the channel coexist in both open orders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): close the freeze report ack race and stop shipping raw ids (MUL-5345) Two review findings on #5989. Acking on hand-off was not acking on delivery. `onCaptured` fires when posthog.capture() returns; the request is still in flight, and posthog-js exposes no delivery callback to wait on (`CaptureOptions` has `send_instantly` and `transport`, nothing else). Deleting the breadcrumb there loses the report whenever the app freezes again or is killed in that gap — the same MUL-4115 failure the ack protocol was added to prevent. The flush now waits out a grace window before acking: if the process dies inside it the timer never fires, the file survives, and the next boot retries. Duplicates are the accepted trade and are trivially deduped on `breadcrumb_ts`; a lost report is not. Raw identifiers were reaching telemetry. The breadcrumb context was spread wholesale into the event props, so `workspaceSlug`, `tabId` and the absolute `windowUrl` shipped with every report despite the stated "bucketed path only" constraint. Fixed at both ends: the slug and tab id are no longer put into the route context at all (nothing else read them), the sanitizer constructs its result explicitly so a stale renderer's payload can't reintroduce them, `windowUrl` is dropped since it is an install path that can carry the OS username and the bucketed route already says which page it was, and the event props are now assembled field by field so a future context key cannot ship itself. The flush moved into `freeze-flush.ts` to make both behaviours testable: `onCaptured` does not ack, the grace window does, a cancelled window keeps the breadcrumb, and props built from a context still carrying slug/tabId/windowUrl contain none of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(diagnostics): reduce MUL-5345 to route attribution only Scope call from product review: the next hang should answer "which page", and nothing more. Removes the CDP stack capture, the long-animation-frame observer, the operation breadcrumb, and the read/ack delivery protocol added earlier on this branch, along with the spike-derived build note. What stays is the smallest change that gives the two existing hang events a real route. The route reporting had been dead since MUL-4127 (#4996) deleted it along with the PostHog pageview tracker: `getDiagnosticContext` in main kept reading a WeakMap nothing wrote, so a hang report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push to main — the only party alive during a true hang, which cannot ask a blocked renderer anything — and also publishes to the in-renderer watchdog, whose `location.pathname` is that same useless packaged path because the shell runs a memory router. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing, and the workspace slug and tab id are not sent at all; nothing outside diagnostics read them. The sanitizer constructs its result explicitly so a renderer older than this build cannot reintroduce them, `windowUrl` is dropped because it is an install path that can carry the OS username, and the breadcrumb event props are assembled by whitelist rather than by spreading the context. Both hang events now report `path` under the same name, so they group in one query. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): bucket hang routes by known structure, not id shape (MUL-5345) The bucketer guessed which segments were ids by looking at them — UUID, issue key, or all digits. Every id that does not look like one therefore travelled to telemetry intact, and most of ours do not: project, autopilot, agent, member, squad, runtime, skill and attachment ids are arbitrary strings from `paths.ts`. `/acme/projects/p1` bucketed to `/:slug/projects/p1`, and `/acme/runtimes/machine%2Fruntime/runtime/runtime%20one` came through completely unchanged. It now matches structurally against the known route shapes, so a `:param` slot is whatever occupies that position regardless of how it is spelled or encoded. Where several patterns fit, the most literal one wins, which keeps `agents/new` the create page rather than an agent whose id happens to be "new". An unmatched path is masked (`/:slug/issues/*`, `/:slug/*`) rather than passed through: a route we do not know is exactly the case where an id cannot be told from a page name, so nothing from it travels. That makes a route added to paths.ts without being added here a loss of detail instead of a leak. To stop the list falling behind quietly, a parity test walks the real path builders — not a copy of them — and asserts that no builder leaks its slug or ids and that none falls back to the mask. Removing a single route from the table fails it with the builder named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7cc763bbfb |
fix(runtimes): show runtime aliases across user-facing surfaces (MUL-5248) (#5881)
Several UIs rendered the raw daemon name (runtime.name) and dropped the user's custom_name alias: the Skills "Copy from runtime" selector, the Skills list/detail source, the agent creation runtime chip, the agent runtime filter, the agent overview, the skills/MCP runtime hints, the runtime delete confirmations, the desktop runtime window title, the onboarding runtime cards/hints, and the task transcript chip. Route every user-visible runtime label through the shared display contract: - runtimeDisplayLabel (alias + provider) for standalone labels - runtimeDisplayName (alias only) where a provider icon/text sits beside it - machine.title + runtimeRowLabel(runtime, machine.title) inside machine groups The Skills "Copy from runtime" selector is now machine-grouped via the shared buildRuntimeMachines/runtimeRowLabel helpers instead of a flat raw-name list; runtime.name stays the source of identity for hostname parsing, grouping, search, and protocol payloads only. Add a conventions rule (en + zh) forbidding raw runtime.name in user-visible JSX/i18n/Select labels/document titles. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1fef98c24f |
fix(desktop): open in-app links in a tab instead of the browser (MUL-5208) (#5826)
* fix(desktop): open in-app links in a tab instead of the browser (MUL-5208) A link written as an absolute URL on this deployment's own origin (`https://<app-host>/acme/issues/1` — what "copy link" produces, and what agents paste into chat) fell through openLink's external branch to window.open, which Electron routes to shell.openExternal. Clicking an issue link in Desktop chat therefore opened a browser window instead of a tab. openLink now resolves such a URL back to its in-app path and takes the same route a relative path does. Backend-served prefixes (/api/, /_next/) stay external so attachment downloads keep working. Two supporting fixes the change depends on: - The `multica:navigate` event had no listener on web, so in-app paths in content were dead links there; normalizing app URLs would have extended that to every pasted app URL. The web platform layer now answers the event with a router push. - The desktop handler opened every path inside the active workspace's tab group. A cross-workspace link now goes through switchWorkspace, matching what the navigation adapter already does for pushes. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): scope in-app link conversion to workspace pages, answer it in issue windows Review follow-ups on MUL-5208. 1. The non-page exclusion list only covered /api/ and /_next/, so a same-origin /uploads/* link — local-storage attachments, served by the backend and proxied by web — was routed as an app page, opening a dead tab instead of the file. Replaced the deny-list with the app's own routing model: an absolute URL converts to an in-app path only when its first segment is a slug a workspace could own, which the existing reserved-slug list (shared with the backend) already answers. /api, /uploads, /_next, /favicon.ico and the pre-workspace routes all stay external without a second list to keep in sync. 2. A dedicated issue window derives the same app origin from its adapter but had no multica:navigate listener, so a same-origin link there became a silent no-op (it used to reach the browser). The window now answers the event: another issue opens in place — matching what its adapter push and mention chips already do — and any other app page, which this single-route window cannot host, goes to the browser. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
257e5e4363 |
fix(desktop): make dev diagnostics best effort (MUL-5148) (#5794)
* fix(desktop): make dev diagnostics best effort Co-authored-by: multica-agent <github@multica.ai> * test(desktop): cover the destroyed dev-log sink guard --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e8746c5030 |
fix(onboarding): eliminate Desktop runtime-step false-negative flash + parallelize daemon version detection (MUL-5119) (#5756)
* perf(daemon): parallelize runtime version detection during registration (MUL-5119) Registration probed each agent CLI's `--version` serially, so total latency was the sum of every probe. On an onboarding host with several coding tools installed that stacked into many seconds before runtimes registered — long enough that the desktop runtime step timed out into its empty 'no runtime found' state while the daemon was still working. Fan the probes out with a bounded errgroup so total latency tracks the slowest single probe instead of their sum. Each probe still self-heals a vanished pinned path and re-detects the live version (no cross-registration caching, so an in-place upgrade is still reported correctly); failures are logged and skipped as before. Results are sorted by provider for a deterministic payload. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): stop the runtime step flashing 'no runtime found' while the daemon probes (MUL-5119) The runtime step flipped from scanning to the empty 'no runtime found' state on a fixed 5s wall-clock, so a machine that does have coding tools installed saw a false-negative flash whenever registration outlasted the timeout (cold start, slow/wedged CLI, many CLIs). Gate the empty flip on a desktop-only `runtimesPending` signal derived from the local daemon's live status (booting, or running with agent CLIs detected on the host): while pending, keep the scanning skeleton past the soft timeout. An absolute hard-timeout ceiling still guarantees a fallback so a wedged probe can't pin the step on the skeleton forever. Web omits the signal and keeps the plain wall-clock timeout. Also drop the two dead/duplicated affordances on the step: the permanently disabled 'Start exploring' button now renders only in the found phase, and the empty state's duplicate footer 'Skip for now' is removed in favour of its own Skip card. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
216aee5629 |
[MUL-5125] Add daily Desktop/Web usage and runtime reporting (#5763)
* feat(analytics): add daily client usage reporting (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): clarify daily usage semantics (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): resolve usage review blockers (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2f111037d2 |
feat(desktop): tab presentation by object identity (MUL-4370) (#5661)
* fix(nav): derive route icons from the URL across all nav surfaces (MUL-4370) The same route rendered different icons in the sidebar and the desktop tab bar because the mapping was maintained in three places. Projects had no tab icon at all; autopilots/chat/squads/usage fell back to ListTodo. Establish one contract instead: `@multica/core/paths` maps a route segment to a stable icon *name* (React-free), and `@multica/views/layout` maps that name to a Lucide component. Every nav surface — sidebar, desktop tab bar, and the search palette — resolves through `routeIconForPath(path)`, so a route cannot render two different icons. Crucially the icon is now derived, not stored. `TabSession.icon` is removed, so persisted tab state can no longer hold a stale icon name: a user who had an /autopilots tab from an older build gets the correct icon after upgrade rather than the one that was persisted. Legacy `icon` values in v4 payloads are ignored on rehydration and dropped on the next write. Builds on the design in #5204 by LiangliangSui. Tests: stale/unknown/absent persisted icon on rehydration, derived icon rendering per route in the tab bar, name→component registry totality, and nav-route icon coverage. Co-authored-by: multica-agent <github@multica.ai> * feat(desktop): tab presentation by object identity, not route segment (MUL-4370) Replace the "route segment → icon" tab mapping with a semantic Tab Presentation Contract: a tab's leading visual and title are derived live from its URL + the query cache, so a tab shows *what it points at*, not the module it lives under. - core `parseTabSubject(url)` classifies a URL as page / resource / actor / container (inbox, chat) / flow / unknown, purely (no React, no Lucide). - core `resolveTabPresentation(subject, data)` maps that + cached entity data to a leading visual (issue StatusIcon, ProjectIcon, ActorAvatar, or a type icon) and a title spec. Exhaustive: a new route forces an explicit choice. - views `useTabPresentation` reads the cache (enabled:false, no fetch from the tab bar) and `ResourceLeadingVisual` renders it in a fixed 16×16 slot. - Containers keep their icon; only the title tracks the selection (`/inbox?issue=`, `/chat?session=`), so an inbox-opened issue reads differently from a direct issue tab. - Titles are plain text; the project 📁 and autopilot ⚡ glyphs are dropped from document.title. - Pin no longer replaces the resource visual. Persisted `tab.icon` cleanup from the prior revision is kept; the active tab persists its resolved title as a first-frame fallback (the document.title→observer path is removed). Supersedes the route-segment approach in #5204 per the agreed PRD. No schema, API, or migration changes; reuses existing queries/caches. Tests: table-driven parseTabSubject over every desktop route; the URL/data → visual+title matrix incl. pending/loading, containers, unknown, runtime custom-name; views cache-integration; tab-bar pin + active-tab title persistence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): archived inbox title sync + attachment filename in tab (MUL-4370) Addresses two PRD gaps from review: 1. Archived Inbox selection now syncs the tab title. `parseTabSubject` captures `?view=archived` on the inbox subject, and the presentation hook resolves the selection against `archivedInboxListOptions` (its own cache, the one the InboxPage populates) instead of only the active list. An `/inbox?view=archived&issue=<id>` tab now shows the archived item's title — issue (`identifier: title`) or non-issue (display title) — and, being purely URL+cache derived, restores correctly on refresh. Previously it fell back to "Inbox" and persisted that wrong title. 2. Attachment tabs use the filename. `parseTabSubject` captures the `?name=` the preview route already carries; the resolver shows the filename as the title and picks a file-type icon from its extension (image/video/audio/ archive/code/text), falling back to the generic File glyph + "Attachment" label only when the name is missing. Tests: parseTabSubject archived/name cases; the presentation matrix for attachment filename/extension + missing fallback and iconForAttachment edge cases; views cache-integration for archived issue/non-issue selection (must resolve against the archived list, not the active one) and attachment filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
181e21f2d0 |
fix(desktop): commit staging env with explicit VITE_APP_URL (MUL-5025) (#5680)
* fix(desktop): commit staging env with explicit VITE_APP_URL (MUL-5025) Staging desktop dev relied on a hand-maintained local .env.staging, where the web-origin var was misnamed VITE_WEB_URL. Desktop only reads VITE_APP_URL, so appUrl silently fell back to the API-origin derivation and copy-link URLs pointed at multica-api.copilothub.ai (404). Track apps/desktop/.env.staging with the correct names (mirroring the mobile precedent), and fill in mobile's EXPO_PUBLIC_WEB_URL now that the staging web host is committed rather than living on a teammate's machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(desktop): mark staging envs internal, clarify VITE_APP_URL semantics (MUL-5025) Per review: state in both tracked .env.staging files that the staging environment is internal (not a public support target), and spell out that VITE_APP_URL is the web app origin for copy-link/open-in-browser URLs, never the API host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
964b9269de |
fix(desktop): restore window state
Remember the main window size, position, and maximized or fullscreen state between launches. Keep the window visible when display settings change. |
||
|
|
612b2db3b9 |
fix(desktop): exclude dist/** from multi-arch package contents
Merging after independent reviews confirmed the fix and all CI checks passed. |
||
|
|
1507997272 |
fix(agent): stop agents shipping local-path links, make Desktop 404 recoverable (MUL-4899) (#5557)
Agents were writing runtime-local paths into deliverables as clickable links (`[screenshot](/Users/agent/work/shot.png)`). Two root causes, both fixed here. A. The brief never stated the delivery contract. Add an always-on delivery invariant (outside writeOutput's kind switch, so no task kind can inherit none) plus a per-surface file-delivery line for each of the five surfaces. Chat splits into two: `attachment upload` works only on web/mobile chat, never on an IM channel, so ChatChannelType is now threaded into TaskContextForEnv. The claim path only ever looked up Slack bindings, so a Feishu session reported as a web chat and got upload guidance for a channel that cannot carry attachments. Probe every channel type. The chat policy is two independent layers and stays that way: delivery keys off "is there a channel at all"; the `chat history` / `chat thread` commands stay Slack-only because both endpoints are hardwired to h.SlackHistory and there is no Feishu reader — ChatInThread only selects between those two commands, so it stays Slack-only too. Add a CLI hard-fail lint on `issue comment add` / `issue create` / `issue update` as the enforcement backstop. Scoped narrowly, since a false positive blocks a real deliverable: agent task context only (a human's PAT run is untouched), real CommonMark link/image/autolink destinations only via goldmark (a path in a code span or fence — how an agent quotes a path it is discussing — is structurally invisible), and three high-confidence signals only (`file://`, inside the workdir, or an existing local file). A bare `/foo` is a valid origin-relative URI and is deliberately allowed. `issue update` has no --attachment flag, so its hint redirects to `comment add` rather than naming an argument it rejects. B. Desktop presented the resulting router 404 as an unknown crash. 8 of 18 desktop_route_error reports were users clicking such a link and being told the app broke and to file a bug. Split the 404 into a first-class Not Found view: no crash framing, no Report error. Its recovery entry comes from the tab store's active workspace, never from the failed pathname — deriving a slug from `/Users/me/shot.png` yields "Users" and a button to `/Users/issues`, a second 404. Also add a will-navigate trusted-origin guard via the shared loadRenderer (main + issue windows). This is origin hardening only, NOT the mechanism for in-app links: client-side routing never fires will-navigate, so app paths never reach it. Issue windows need no 404 work — their router only accepts paths validated by parseIssueWindowPath and they do not listen for multica:navigate, so a bad path cannot reach them. Server-side completion observation is metric/log only and never blocks: it is lexical (`file://` + task work_dir prefix) because the server cannot stat the daemon's filesystem, and the metric label is a closed enum so no path or reply text reaches Prometheus. Verified: pnpm typecheck/lint/test (3582 tests), go vet, full Go suite including new claim-path integration tests. cmd/multica was verified outside the daemon workdir — inside one, 93 of its tests fail identically on origin/main because the suite walks up and finds the runtime's own task marker. 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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
2d13b26fcc | feat(desktop): add automatic update preference (#5380) | ||
|
|
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> |
||
|
|
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) | ||
|
|
4efcfb96e3 | feat(ui): establish surface system (#5248) | ||
|
|
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
|
||
|
|
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> |
||
|
|
3790ca78e7 |
fix(desktop): run git describe without a shell so version derivation works on Windows (#5097)
#5057 restricted version derivation to `git describe --tags --match 'v[0-9]*'`, but the command was passed to `execSync` as a shell string. On Windows the shell is cmd.exe, which does not strip the POSIX single quotes around 'v[0-9]*', so git received the quotes literally, matched no tag, fell through to `--always`, and the version degraded to the `0.0.0-g<hash>` fallback. That is what shipped a `0.0.0-gc05b67ae4` Windows Desktop build (electron-builder `--publish always` then auto-created a bogus release) during the v0.3.41 release, even though the tag was sitting exactly on HEAD. Linux/macOS were unaffected because /bin/sh strips the quotes. Fix: invoke git with an argv array via execFileSync in every version-derivation path, so the match pattern reaches git as one literal argument regardless of platform: - apps/desktop/scripts/package.mjs (Desktop version → electron-builder) - apps/desktop/scripts/bundle-cli.mjs (bundled CLI ldflags version) - apps/desktop/src/main/app-version.ts (dev-mode version fallback) The Makefile is intentionally left as-is: make's `$(shell ...)` always runs via /bin/sh (even on Windows) and the CLI release runs on Linux, so its single quotes are stripped correctly. Tests: export `deriveVersion` and `DESCRIBE_ARGS` and add coverage that runs the real `git describe` against throwaway repos (clean semver tag, semver tag chosen over a nearer non-semver tag, and the no-tag fallback), plus a structural check that the match pattern is a bare argv token with no embedded quotes. The prior suite only unit-tested the `normalizeGitVersion` string transform, which is why this slipped through. MUL-4256 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c8cfd0a214 |
fix(desktop): restrict git describe to semver tags for version derivation (#5057)
Non-semver tags (e.g. release-train tags) could become the nearest match for `git describe --tags`, producing a version string that is not a valid semver prefix. Restrict describe to `v[0-9]*` tags across the CLI ldflags, desktop bundling, and app-version paths so the resolved version always has a `major.minor.patch` shape. |
||
|
|
3cb5dc3ad6 |
chore(analytics): retire redundant PostHog tracking (MUL-4127) (#4996)
* chore(analytics): retire redundant PostHog tracking (MUL-4127) PostHog had become a chaotic, largely-unused second copy of data we already query from the DB and Grafana. Remove the redundant instrumentation. Server: every product event (signup, workspace_created, issue_created, issue_executed, chat_message_sent, team_invite_*, onboarding_*, agent_created, cloud_waitlist_joined, feedback_submitted, contact_sales_submitted, squad_created, autopilot_created) is now in metricsOnlyEvents, so metrics.RecordEvent still increments the Prometheus/Grafana counter but no longer ships to PostHog. DB rows remain the source of truth. Runtime/autopilot/ agent_task lifecycle were already Prometheus-only. Frontend: delete the PostHog-only funnel instrumentation — $pageview (+ web and desktop trackers), download_intent_expressed/page_viewed/initiated, the onboarding_started mirror, onboarding_runtime_path_selected/detected, feedback_opened, and source_backfill_*. The source-backfill modal itself stays (it PATCHes the questionnaire to the DB). Kept on PostHog (frontend only): $exception autocapture and the client_crash / client_unresponsive stability telemetry (no DB equivalent), plus $identify/$set. captureSignupSource (attribution cookie) stays — it still feeds the signup_source Prometheus label. Verified: pnpm typecheck, pnpm lint (0 errors), vitest (core/views/web/desktop), go test ./internal/analytics/... ./internal/metrics/... Co-authored-by: multica-agent <github@multica.ai> * docs(analytics): fix stale PostHog references after MUL-4127 (review follow-up) Addresses review of #4996 — three spots still described server events as active PostHog signals after they became metrics-only: - docs/analytics.md: issue_executed is no longer a PostHog success signal; it is Prometheus-only (multica_issue_executed_total) + issue.first_executed_at, in both the event contract and the Reconciliation section. - docs/analytics.md: the signup $set_once person properties (email, signup_source) are no longer emitted — signup is Prometheus-only; only the bucketed signup_source survives as the multica_signup_total label. - server/internal/metrics/business_events.go: RecordEvent doc comment no longer claims it ships product events to PostHog / "PostHog is reserved for user/product-behaviour events" — every server event is now metrics-only. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
12d901638e |
fix(desktop): resolve electron-vite bin via PATH in dev script (#4992)
Under the hoisted linker (node-linker=hoisted) electron-vite's bin only lands in the repo-root node_modules/.bin, so the hardcoded apps/desktop/node_modules/.bin path fails. Use envWithLocalBins to put both .bin directories on PATH and invoke electron-vite by name. |
||
|
|
84a5853363 |
fix(desktop): restore correct filename in save dialog for attachment downloads (#4296)
Adds an Electron `will-download` handler that forwards the filename Electron parsed from the server's `Content-Disposition` header (`item.getFilename()`) into the native save dialog, so desktop attachment downloads no longer default to `download.txt`.
Registration is guarded with a module-level `WeakSet<Electron.Session>` so the handler is installed at most once per session, even when macOS re-invokes `createWindow()` via `app.on("activate")`.
Fixes #4153
|
||
|
|
03828015ca |
fix(feedback): validate response and pass error kind (#4633)
Wire structured feedback kind through the frontend/core feedback path so desktop route-renderer errors submit as bug feedback, and bring the feedback client onto parseWithFallback. MUL-3768 |
||
|
|
6dcf82a58a |
feat(desktop): isolate pnpm dev:desktop per worktree (MUL-3724) (#4598)
* feat(desktop): isolate pnpm dev:desktop per worktree (MUL-3724) Two worktrees could not run pnpm dev:desktop at once: both grabbed the renderer port 5173 and the single-instance lock keyed by the app name "Multica Canary". The env hooks to override each already existed (DESKTOP_RENDERER_PORT in electron.vite.config.ts, DESKTOP_APP_SUFFIX in src/main/index.ts) but nothing derived per-worktree values. A new dev launcher (scripts/dev.mjs) derives both from the worktree path for linked worktrees only — reusing the same cksum%1000 offset as scripts/init-worktree-env.sh, so renderer port is 5173+offset and the app becomes "Multica Canary <folder>" with its own userData/lock. The primary checkout is untouched; explicit env vars still win. Backend targeting is unchanged (apps/desktop/.env*). Also: brand-dev-electron honors the suffix, turbo globalEnv passes it through, and CONTRIBUTING documents the flow. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): make worktree dev port/suffix collision-safe (MUL-3724) Addresses code review on #4598: - Renderer port base 5173 → 5174 so a worktree whose offset is 0 (e.g. cksum("/tmp/multica-3494") % 1000 === 0) no longer collides with the primary checkout's default 5173. - DESKTOP_APP_SUFFIX is now "<folder>-<offset>" instead of just the folder name, so worktrees that share a basename at different paths (or names that slug to the same fallback) get distinct single-instance locks. Without it the second Electron was still blocked by the shared lock. - Tests: offset-0 port guard, and same-basename-different-path disambiguation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6d0e875dbb |
feat: add opt-in react-grab dev element inspector (web + desktop) (#4381)
* feat(web): add opt-in react-grab dev element inspector Loads the react-grab overlay (hold ⌘C / Ctrl+C + click to copy an element's source path + component stack) only when REACT_GRAB is set in a local, gitignored apps/web/.env.local. Both the NODE_ENV and REACT_GRAB guards are evaluated server-side in the root layout, so the <Script> tag is omitted from the HTML for anyone who hasn't opted in — no effect on other developers or production. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(desktop): add opt-in react-grab dev element inspector Mirrors the web wiring for the Electron renderer: injects the react-grab overlay (hold ⌘C / Ctrl+C + click to copy an element's source path + component stack) only when VITE_REACT_GRAB is set in a local, gitignored apps/desktop/.env.development.local. Guarded by import.meta.env.DEV so the branch is tree-shaken out of production builds; never activates for other developers. No CSP/sandbox blocks the unpkg script (webSecurity is off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): unify react-grab opt-in var to VITE_REACT_GRAB Use the same env var name as the desktop renderer so one variable name controls both apps. The desktop renderer is bundled by Vite, which only exposes VITE_-prefixed vars to client code, so the shared name must carry the VITE_ prefix; web reads it server-side where the name is unconstrained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
27fcbb015f |
Polish desktop sidebar motion
Polish desktop chrome/sidebar alignment and add motion-based transitions for left and right sidebars. |
||
|
|
dd9e7bf19d |
fix(desktop): coerce commit-hash versions to valid semver (MUL-3314) (#4183)
* fix(desktop): coerce commit-hash versions to valid semver normalizeGitVersion checked only the first character (/^\d/) to tell a real version from a bare commit hash. A hash beginning with a digit (e.g. '2f24057b') passed that check and was stamped as the app version, but bare '2f24057b' is not valid semver, so electron-updater threw on launch. Require a full major.minor.patch prefix; anything else (including a digit-leading hash) falls back to 0.0.0-<hash> as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(desktop): prefix bare-hash fallback with `g` for valid semver normalizeGitVersion coerced an untagged build's bare commit hash into `0.0.0-<hash>`, but that is not guaranteed valid semver: an all-digit short hash with a leading zero (e.g. `0123456`) produced `0.0.0-0123456`, and a numeric semver pre-release identifier must not have a leading zero. electron-updater then threw on launch for exactly the untagged builds this fallback exists to protect. Prefix the hash with `g` (mirroring `git describe`'s own `g<hash>` shorthand) so the pre-release is always a single alphanumeric identifier. Add a regression test for the all-digit leading-zero case. Addresses PR #4183 review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |