Add Kimi thinking level support - #1
Open
bowser-bot wants to merge 52 commits into
Open
Conversation
… (MUL-5714) (multica-ai#6380) * fix(issues): unsubscribe in one click when an issue has no sub-issues (MUL-5714) The unsubscribe control was always a dropdown, so leaving an issue with no sub-issues cost two clicks and offered a second option pointing at a sub-tree that does not exist. Collapse it to a direct button once the child query says there are no children. That button takes the root-only route (opt_out_scope='issue'), never the subtree one. Even at zero children the two are different server writes: a subtree tombstone also keeps FUTURE children from re-subscribing the user, so declining one issue must not silently opt someone out of a tree that does not exist yet. While the child count is still unknown the menu stays — unlike the button it never picks a scope on the user's behalf, which matters because childIssuesOptions refetches on every mount. Two more silent states around the same control: - The subscribers query's empty default read as "not subscribed" for everyone, so an already-subscribed user saw Subscribe until the query resolved, and a click in that window sent a subscribe. Gate the control on subscriptionKnown. - A failed subscribe/unsubscribe rolled its optimistic patch back and said nothing, which looks exactly like a dead button. Add a failure toast, and a success toast for the subtree unsubscribe — that one is not optimistic, so nothing on screen confirms the click. Controls are disabled while a mutation is in flight and when there is no auth user. Direct toggles also take an in-flight ref: React Query flushes isPending in a microtask, so two clicks in one tick both reach an enabled button, and overlapping toggles are what the mutation's whole-list snapshot cannot survive — the second snapshots the first one's patch and rolls back to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): gate the subscriber picker on a resolved query too (MUL-5714) Review catch: the subscribe button waited for the subscribers query, but the picker behind the avatar group did not. Every checkbox in it is drawn from the same `data ?? []`, so an unresolved query rendered everyone as unsubscribed, including people who were subscribed. Clicking one of those rows is not a harmless no-op. An explicit subscribe rewrites the target's reason to 'manual' and clears any opt_out_scope (SubscribeToIssueExplicitly, server/pkg/db/queries/subscriber.sql), so a stray click during the load window discards a delegated subscription or someone's deliberate opt-out — and the rows cover other members and agents, not just the current user. Pass `togglesDisabled = !subscriptionKnown || togglePending || !user?.id` and apply it to both the member and agent rows. Also drop `subscribersLoading` / `subscribersError` from the hook's return. With the picker gated, the component needs only `subscriptionKnown`, and exposing state for tests to assert on is not a reason to keep an API. The query-failure test now reads the query's own state instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
…2 initiator prose (MUL-5721) (multica-ai#6382) * refactor(daemon): drop duplicate UUID commands from per-turn comment hints (MUL-5721) OPT-1 from the MUL-5721 measurement: within one hint, a second full `multica issue comment list <uuid> ...` command restated the issue UUID (and, in the resumed hint, both anchors) for no routing value. The warm hint's issue-wide catch-up and the cold hint's roots scan become flag swaps on the thread command they follow; the resumed hint loses its anchor-restating sentence (the read command carries the thread anchor, the reply cookbook carries the trigger id as --parent). Fixture-measured: warm hint 555->481, resumed 551->417, cold 429->396; reply-turn totals -74/-134/-33 B. Ownership prompt and the reply cookbook (with its concrete --parent command) are untouched. Every dropped duplicate gets a deny assertion so it cannot silently return; the demoted fallbacks get semantic anchors in its place. Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): compress Task Initiator attribution prose (MUL-5721) OPT-2: the block's 400 B attribution paragraph carried two derivable restatements ("in a workspace many people can reach", "so this attribution does not widen what you can read or write"). The compressed paragraph keeps every rule: per-person privacy/access rules apply, the initiator (not the runtime owner) is who you are answering, credentials stay scoped to the runtime owner, and the initiator may not see everything you can. Both MUL-2645 test-pinned phrases survive verbatim; the fact line and heading are untouched. Fixture-measured: block 513->370 B (member+email form). The two previously unpinned surviving rules gain anchors ("is who you are answering", "do not assume the initiator can see everything you can"). Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): restore initiator read/write authorization boundary (MUL-5721) Review must-fix (multica-ai#6382): the compression treated "attribution does not widen what you can read or write" as derivable from credential scoping and dropped it. It is an independent boundary: credential scoping names whose credentials run, the visibility sentence constrains disclosure — neither stops the model from treating an initiator's request as extra authorization. Restored as a short explicit negation with a semantic anchor. Initiator block 370 -> 440 B (still -73 vs the original 513); typical reply turn 2,191 B (net -147 for the PR). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
… (MUL-5722) (multica-ai#6383) * fix(agent): raise the agent stream scanner cap to 32 MiB and share it (MUL-5722) Codex serializes an entire thread into the single `thread/resume` response line, so a long thread crossed the per-backend 10 MiB bufio.Scanner cap and failed the resume with "bufio.Scanner: token too long". Because that classifies as agent_error.process_failure, the session pointer survives and every later turn resumes the same oversized thread — the task is stuck for good. This is GH#4520 recurring: multica-ai#4563 bounded the hang and the orphan process, but the cap follow-up it promised was never done. Raise the bound to 32 MiB and route every line-delimited agent transport through one newAgentStreamScanner constructor. The cap was copy-pasted into 17 sites and had already drifted (pi at 32 MiB, everything else at 10 MiB), which is how multica-ai#4563's fix reached only one backend. The three tests that deliberately overflow the scanner now size their payloads from the constant, so raising the cap again cannot silently downgrade them into plain oversized-line passes. This is the containment layer only. A thread can still outgrow any fixed cap, and the recovery path is still missing: overflow is reported as a transport error, which blocks the thread/start fallback, and codex never sets ResumeRejected so the daemon's fresh-session retry does not fire either. Those remain open on MUL-5722. Co-authored-by: multica-agent <github@multica.ai> * fix(agent): route the QwenPaw ACP stream through the shared scanner (MUL-5722) QwenPaw (multica-ai#5986) landed on main after this branch was cut, carrying its own bufio.Scanner with the old 10 MiB cap. Merging as-is would have left one backend still on the cliff this PR exists to remove — the same way multica-ai#4563's fix originally reached only one backend. Rebased onto main and routed it through newAgentStreamScanner. No stray per-backend cap remains: the only bufio.NewScanner calls left in the package are the model-listing and codex session-file parsers, which are deliberately out of scope. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
…ca-ai#6211) * feat(chat): queue follow-up messages during active runs * fix(chat): preserve queued message compatibility * fix(chat): harden queued task consistency * fix(chat): protect legacy and mobile queue state * fix(mobile): refresh stop handler on task promotion * fix(mobile): preserve queued chat state * fix(chat): hide queued prompts from legacy clients * test(chat): read queued channel input from paged transcript * fix(chat): protect legacy and mobile queue state * fix(mobile): refresh stop handler on task promotion * fix(mobile): preserve queued chat state * fix(chat): hide queued prompts from legacy clients * test(chat): read queued channel input from paged transcript * fix(chat): hide queued inputs from paged transcripts * test: prove queued messages do not consume cursor pages --------- Co-authored-by: TeAmo <liu.junhui3@iwhalecloud.com>
Co-authored-by: multica-agent <github@multica.ai>
… (MUL-5442) (multica-ai#6381) * refactor(daemon): rewrite Background Task Safety to its judgment form (MUL-5442) Owner-authorized judgment rewrite per the Claude 5 context-engineering guidance: state the platform fact and the boundaries an agent cannot infer, drop the enforcement details a frontier model derives. The section is now three paragraphs: (1) the fact everything derives from — turn exit is task-terminal, no wakeup exists, never background-and-yield — with the foreground-collect, synchronous-fallback and no-standing-by consequences stated once; (2) the external-systems/CI boundary: not run-owned, the named --watch ban (kept because MUL-5223 proved the principle alone did not stop CI-watching), merge-gate is not acceptance criteria, the hand-off phrasing, and the single explicit-ask exception; (3) the persistent-service handoff contract, review-locked verbatim. Deliberately dropped as derivable: the run-owned work enumeration, the tool-promise enumeration, the wait/collect split rule, the persistent-service scope bullet, auto-merge and snapshot elaborations. Both pin lists rewritten to the surviving semantic anchors (21 each); retired pins are listed in the test comments with the rationale. 2,980 -> 1,931 (-1,049). Incident history stays in the Go comment where it costs no context. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): delete the duplicated CI exception, pin the full ban clause, sync the comment ledger (MUL-5442) Review catches by Elon on the judgment rewrite, all three accepted: 1. The old exception bullet survived below the handoff paragraph — the CI exception appeared twice, the second copy scope-widened by its position, and every substring pin stayed green. Deleted; both BTS tests now count-guard exactly one 'The one exception' occurrence. 2. The compound watch/poll ban was pinned only by its first command; the full clause is the MUL-5223 boundary, so the pin now covers all three members. 3. The Go comment ledger still described the pre-rewrite bullet model (retired pins as kept, a forward reference that no longer exists). Rewritten to the three-paragraph model so the safety-decision history reads true. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
from
August 5, 2026 05:35
2352ca1 to
1a61cf6
Compare
…MUL-5737) (multica-ai#6416) * docs(desktop): distinguish Desktop desktop.json from the CLI config (MUL-5737) Users self-hosting on Windows were editing the CLI's ~/.multica/config.json and expecting Desktop to pick up server_url. Desktop only reads ~/.multica/desktop.json (apiUrl), so the edit silently had no effect. - give the per-platform path, including the Windows one, which was never spelled out; - call out that desktop.json is not config.json and that the key names differ; - document the missing-file vs invalid-file behaviour, since only the invalid case surfaces an error and the silent case is what users actually hit; - add the Windows filename/encoding traps (Notepad .txt, PowerShell BOM). Refs multica-ai#6399 Co-authored-by: multica-agent <github@multica.ai> * docs(desktop): fix the Windows filename check to be valid PowerShell (MUL-5737) The verification command was cmd.exe syntax sitting in a PowerShell-led section: %USERPROFILE% is not expanded by PowerShell, so 'dir %USERPROFILE%\.multica' fails on a literal path. It also claimed the listing should contain exactly desktop.json, which is wrong — a normal .multica holds config.json, desktop_prefs.json and profiles/ too. Replace it with a scoped PowerShell check that surfaces the actual filename, so a Notepad-created desktop.json.txt shows up directly rather than only reading as 'not found'. Also mark the per-platform paths as defaults, since a moved or redirected home directory won't match C:\Users\<you>. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): give the phone back the space issue detail was spending on chrome
Issue detail reads as cramped on a phone because three separate things each
take a slice of a 393px screen and none of them was decided for that width.
Content gutters. `px-8 py-8` is a comfortable reading margin on a desktop
column; on a phone it spends 64px — 16% of the width — on nothing. Below `md`
they drop to 16px.
A pinned composer. `sticky` is a stored preference, answered by a user sitting
at a wide screen. Applied to a phone it costs ~15% of the viewport at every
scroll position on a surface people mostly read, and it parks the composer
under the chat launcher — covering its own send button. It now yields below
`md` without rewriting the preference, so widening the window restores it.
The launcher's corner. `tokens.css` already declares that corner off-limits to
page content and ships `--chat-launcher-clearance` to yield by; exactly one
site honoured it. Two sibling utilities cover the other two ways of reaching
the corner (scroll content ending in it, a centred overlay growing into it),
and the six floating toolbars plus the issue-detail column now use them.
Also fixes two mobile bugs found on the way:
Inbox wrapped `IssueDetail` in its own `overflow-y-auto`, which collapsed the
detail's inner scroller to content height. That took its header — and the
done/pin/more/sidebar actions in it — out of the pinned position, made
`position: sticky` inside it a no-op, and pointed both scroll restoration and
the timeline virtualizer at an element that never scrolls. Removing the wrapper
would have left two stacked 48px bars, so the inbox's back control moves into
the detail's own header through a `leading` slot on `PageHeader`. The detail
owns the whole phone screen in four states, so the slot is threaded through all
four — loaded, loading, not-found and crashed — since missing one strands the
reader with no way back.
The sidebar is a Sheet on a phone and never closed itself, so tapping a nav
item navigated underneath it and read as a dead tap. It now dismisses on
`pathname`, which covers the nav groups, the pinned items and the workspace
switcher's programmatic push in one place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(inbox): keep the way back for a mobile notification with no issue
`InboxItem.issue_id` is nullable, and `detailContent`'s ternary has three
branches, not two: a null-issue row falls to a plain notification block, not
to `null`. So the mobile `{detailContent ?? mobileBackBar}` guard was dead
code — `detailContent` is truthy whenever a row is selected — and that block
has no header, which is where the back control now lives.
The result was a dead end: opening a quick_create_failed or
quick_create_unconfirmed item on a phone left the reader with the notification
body, an archive button, and no route back to the list.
The two kinds of selection get their chrome from different places, so the
mobile branch now splits on `issue_id`. An issue hands the whole screen to
`IssueDetail`, which owns its scroller and takes the back control through
`leadingAction`. A notification keeps a bar and a scroll body of its own,
since a plain block has neither.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
from
August 5, 2026 06:14
1a61cf6 to
1745a5e
Compare
* feat(agent): add Reasonix runtime Co-authored-by: multica-agent <github@multica.ai> * fix(agent): make Reasonix sandbox host-adaptive Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…, per-turn carries the commands (MUL-5442) (multica-ai#6421) * refactor(daemon): move the workflow's command templates to the per-turn channel (MUL-5442) Cross-channel dedup, brief side. MUL-5721 measured that every issue-turn variant of the per-turn message already carries the ready-to-run commands with real ids (issue-get line, reading hints, reply cookbook), fresher than the brief's static copies. The six workflow steps now state the loop shape — command names and flag mnemonics only; full templates and every embedded issue UUID leave the brief. The Ownership status commands and the squad-activity call switch to <issue-id> placeholder form. Doctrine pins stay on the brief side (mandatory catch-up, scan-first order, both motivation anchors); command-template pins re-anchor to names and mnemonics. Two design-consequence test updates: the static-catch-up assertion repoints at the doctrine (the full command now lives in every per-turn variant), and the byte-identity non-vacuity guard varies agent identity instead of issue id — because the brief is now deliberately issue-id-independent, which opens the cross-issue prefix-cache door. Co-authored-by: multica-agent <github@multica.ai> * refactor(daemon): point the reply cookbook at Comment Formatting instead of restating it (MUL-5442) Cross-channel dedup, per-turn side. The cookbook's prose restated the brief's Comment Formatting rules (file-first rationale, inline-content and HEREDOC hazards, the full Windows $OutputEncoding mechanics) around the command that already demonstrates the shape. It now keeps the file-first order, the ready-to-run command, the literal-newline rule, and the pointer; the hazard mechanics live once, in the brief section the pointer names. Default cookbook: 639 -> 494 bytes (-145 per reply turn, uncacheable channel); the Windows variant sheds ~200 more. Pins re-anchor from the deleted prose to the surviving anchors (file-first order, the pointer, the command form); the MUL-2904/multica-ai#4182 banned-shape negative guards are untouched. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): narrow the per-turn coverage claim, test the cross-issue invariant, strip the command shape from the guest-visible prohibition (MUL-5442) Review catches by Elon on multica-ai#6421 plus the CI failure, all three related: 1. The steps header overstated what the per-turn message carries — it ships the issue id and ready-to-run context-read commands; other calls are assembled from Available Commands. Reworded to say exactly that (a factual claim in a prompt cannot rely on the reader discovering it is wrong). 2. The cross-issue byte-equality this PR claims as a design benefit is now asserted directly per provider (same stable inputs, different 36-char issue UUIDs, byte-identical brief) — a Contains-based negative cannot catch truncated/transformed/conditional id use. 3. CI: the squad guest-leader contract test bans any runnable 'issue status <issue-id> in_review' shape in guest-visible text; the placeholder rewrite made the leader dispatch rule's NEGATIVE sentence match that ban. The prohibition now states itself without a command form ('do NOT move it to in_review or done on this turn') — negative sentences should not carry copy-pasteable command shapes at all. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
…ca-ai#6398) * fix(channel): make issue commands terminal outcomes * fix(channel): stop handled command media from gating later chat tasks A `/issue` turn is now answered synchronously and excluded from every later input batch, but it still persisted a media deadline, and both session-scoped media gates counted it. The next, unrelated chat message was therefore deferred until the command's attachments bound — or for the full fallback budget on the create-failure path, where no binder ever runs to clear the marker. Give GetChannelMediaPendingUntil and PromoteChannelChatTasksIfMediaReady the same population as the batch seal, so only a turn that can join a batch can gate one. DeferChatTaskForSealedPendingMedia already scopes by task id and needs no change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
…multica-ai#6410) * fix(autopilot): invalidate empty cache for scheduled tasks * test(autopilot): pin the wiring the empty-claim fix depends on TestBackgroundServicesReuseRouterServices asserted that backgroundServices returns h.TaskService / h.AutopilotService — which is the helper's entire implementation — so it stayed green even when main() ignored the helper and constructed its own services again. That is the exact regression it was named for: reintroducing the duplicate-service wiring left the test passing. Replace it with an AST guard over main.go that fails when main() calls service.NewTaskService / service.NewAutopilotService or stops calling backgroundServices(h), plus an anti-vacuity check that the schedule-job registration still lives in main() so the guard cannot pass on a walk that matched nothing. Add a service-level test for the underlying hazard: EmptyClaimCache is nil-safe, so a TaskService that never had EmptyClaim assigned fails silently — the daemon wakeup still fires while the claim path's cached empty verdict survives until the TTL expires. Verified by mutation: reintroducing the original duplicate-service wiring fails the guard, and refactoring the schedule-job registration out of main() fails it too. Co-authored-by: multica-agent <github@multica.ai> * test(autopilot): match the router handler argument, not just the callee name The previous guard only checked that main() called backgroundServices, so backgroundServices(nil) satisfied it — a call that compiles, reuses none of the router's wiring, and nil-derefs at startup. Resolve the variable holding NewRouterWithOptions' *handler.Handler result and require that exact ident as the argument. Reading the name off the assignment instead of hardcoding "h" keeps a rename of that variable from silently weakening the check, and an unrecognizable router assignment now fails loudly rather than leaving the argument check with nothing to compare against. Mutation-verified: backgroundServices(nil) fails, the original duplicate-service wiring fails, moving the schedule-job registration out of main() fails, an unresolvable router assignment fails, and renaming h to routerHandler still passes. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
from
August 5, 2026 07:01
1745a5e to
7e4e571
Compare
…ai#6418) * fix(chat): keep idle sends out of follow-up queue Co-authored-by: multica-agent <github@multica.ai> * fix(chat): preserve the positional queue head Co-authored-by: multica-agent <github@multica.ai> * fix(chat): polish deferred queue states Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): resolve pending index prefix collision Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): update Reasonix logo Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): address logo review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#6362) * feat(i18n): translate issue as the local word for "task" (MUL-5703) zh-Hans 任务, ja タスク, ko 태스크 across the app locales, the landing dictionaries, and the onboarding issue templates. "issue" has no meaning in any of these languages outside dev jargon; the everyday task word is what users already say for this. The agent execution run is a separate user-visible entity, so it keeps a distinct spelling per locale — zh `task` (unchanged), ja 作業, ko 작업 — and never collapses into the same word as the issue. Untouched: API/DB fields, `multica issue` CLI references, the /issue Slack and Lark slash command, English feature names in the changelog archive, and "issue" in the machine-health sense (异常 / 問題 / 문제). conventions.mdx and conventions.zh.mdx carry the new rule so translation PRs stop reverting to English. Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): finish the run/issue split in the landing copy (MUL-5703) Renaming issue to 任务 / タスク / 태스크 left the agent execution run sharing that word in the landing dictionaries — zh.ts had "任务表格" (issue) and "智能体任务" (run) in one sentence. The run now reads `task` / 作業 / 작업 there too, matching the app locales. Each string was classified against its EN counterpart at the same structural path, so "assign tasks" in the hero (which means issues) stays 任务 while "agent tasks" in the changelog becomes the run word. Strings EN cannot disambiguate were decided by hand. Also: the onboarding starter prompts spelled the concept list `workspace / issue / agent / runtime` while the next prompt in the same file already used the new word, and the ja/ko VCS settings copy was still untranslated English showing "issues" on the settings page. Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): keep Chinese fixed compounds, tidy ja/ko grammar (MUL-5703) 后台任务 and 定时任务 are settled Chinese compounds for "background job" and "cron job" — neither is the agent execution entity, so splitting them into `task` was wrong. Japanese: the landing file spaces katakana-katakana compounds, so replacing タスク with kanji 作業 left "エージェント 作業" and "作業 トークン" reading oddly. Reworded to の / closed compounds. Korean: 작업 ends in a consonant, so the subject marker after the parenthetical becomes 이, not 가. Also drops a __pycache__ artifact that git add -A swept in, and ignores Python bytecode so agent tooling cannot commit it again. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…-5739) (multica-ai#6422) Detect provider context exhaustion from Claude Code's structured terminal_reason rather than from its error prose, so a saturated session is retired and the next task cold-starts instead of resuming a conversation that can no longer answer. Refs multica-ai#6402 — does not close it: the captured 2.1.220/2.1.221 frames report is_error alongside terminal_reason, so the specific `completed` escape the issue reports is not reproduced. The structured check is the load-bearing fix and is proven present on the reported CLI version; the composite text predicate is a bounded backstop for uncaptured shapes.
…#6424) * feat(editor): page through an issue's / chat's images (MUL-5752) Opening any image in an issue or a chat session now starts a sequence: the viewer shows "3 / 7", chevrons and Left/Right walk to the neighbouring images, and the ends disable rather than wrap. Images only. PDFs, video, audio and text keep their single-file preview — mixing kinds would mean defining load, keyboard and playback semantics for a "next" that can land on a PDF page. - packages/core/attachments/image-sequence.ts: the ordered sequence for a set of {content, attachments} blocks, plus the image test, the URL to attachment match and the standalone-attachment rule the renderers already applied separately. Built from data, not the DOM, because both the issue timeline and the chat list are virtualized. Shared with mobile. - ImageSequenceProvider hosts one viewer per surface and freezes the sequence on open, so arriving comments cannot shift the index under the reader. An image the sequence does not know (a composer's in-flight upload) still opens on its own. - A frame that fails to load is skipped in the direction of travel with a light toast, so a deleted attachment cannot trap the reader. - Mobile keeps the semantics with its own interaction: the lightbox pages the same sequence by horizontal swipe and shows the same counter. The re-sign hook moves out of attachment.tsx into hooks/use-inline-media-url so the modal can upgrade an auth-gated URL for an image the reader navigated to rather than clicked — without it, paging broke on Desktop and on proxy-mode self-hosts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(mobile): call the chat image-sequence memo unconditionally (MUL-5752) `imageBlocks` sat below the loading and empty-state early returns, so the useMemo only ran on renders that got past both — a rules-of-hooks violation that mobile CI caught. Moved above both returns; an empty `messages` just yields an empty block list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): flashless sequence navigation and preview header alignment - Keep PreviewPanel mounted across navigation; swap the image only after the next frame decodes (useSettledImageURL), and prefetch both neighbours while the viewer is open - Baseline-align filename/type in the header, keep ZoomControls mounted (disabled until measured), move the position counter to a bottom pill - Disabled boundary arrows keep receiving pointer events so the zoom canvas grab cursor can't show over a dead control Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): only keep zoom controls mounted for sequence previews Standalone previews restore the original gate (hidden until measured, hidden for content with no intrinsic size); the always-mounted variant broke tests whose i18n mocks never expected the canvas strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#6428) Tell a chat agent whether it is answering in a shared room or a 1:1, without splitting the cached brief prefix: the room shape rides the existing channel binding row to the claim response, and is stated once per turn in the chat prompt rather than in the static runtime brief. Originally contributed as multica-ai#6390; re-rolled to move the audience fact to the per-turn prompt and to minimize the copy it renders. Co-authored-by: Seacen Zhao <xichangzhao@outlook.com> Co-authored-by: multica-agent <github@multica.ai>
…i#6431) * fix(chat): keep visible queue heads in transcript order Co-authored-by: multica-agent <github@multica.ai> * test(chat): cover cancelled follow-up ordering Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…ca-ai#6433) Two hang fields cost something and told us nothing, so both go. HANG STACK CAPTURE. Shipped in v0.4.13, flag published since v0.4.14, on in production — and every stack it produced was a single entry frame with an empty url. `Debugger.pause` lands on the next JS statement boundary, so when the block is in native code (layout, paint, GC, sync IPC) or the pause dispatches just after the block clears, the frame we get is the next function to run, not the one that blocked. It also only fires for hard hangs, so it sampled a fraction of a signal that was already useless. The price was a CDP debugger channel held open on every renderer for the whole session. The server key goes with it, and that is the part that matters for already-installed clients: v0.4.13–v0.4.18 are fail-closed on this flag, so no longer publishing `desktop_hang_stack_capture` is what makes them stop attaching a debugger. keys_test now pins the key as unpublished — re-adding it would put a flag flip back within reach of a fleet that still can't produce a usable stack. `recovered`. Hardcoded `false` at the only site that sets it. A recovered hang has its breadcrumb cleared and is reported by the in-thread watchdog instead, so the field could never be anything else. A machine upgrading from v0.4.18 can still have a breadcrumb on disk whose context holds a captured stack. `buildFreezeEventProps` builds props by whitelist, so it drops on its own — pinned by a test, since nothing sanitizes frames anymore. What stays: the longtask watchdog, the main-unresponsive breadcrumb with its bucketed route, client_crash, and $exception. Those are the signals carrying the analysis in MUL-5345. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
…tica-ai#6437) Closing the active tab activated a positional neighbour, so opening a detail tab from a list and closing it dropped the user on whatever happened to sit next to it in the strip — new tabs are appended at the end, so that was rarely the tab they came from. Tab groups now carry an MRU activation order (`recentTabIds`, most recent first). Closing the active tab activates the most recently visited surviving tab and falls back to the old positional neighbour only when that order is empty. The order is persisted, so the first close after a restart behaves the same; rehydration re-validates it against live tab ids. Every write that changes a group's tabs or active tab now goes through `reconcileGroup`, which keeps the order free of closed tabs, duplicates, and the active tab itself. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…e-fit (multica-ai#6435) * refactor(editor): move sequence navigation into the header action cluster Arrows floating over the image covered exactly the content being viewed; the prev/counter/next cluster now leads the header's right-side actions, replacing the edge chevrons and the bottom counter pill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): paint new canvas content pre-fitted, without easing Re-fit on a natural-size change now runs in a layout effect and clears isAnimated: sequence navigation is content replacement, so the next frame must appear already fitted instead of rendering once under the previous image's transform and easing to fit when a prior discrete zoom left isAnimated on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ltica-ai#6438) PR multica-ai#6362 added one prose line per language with {{count}} outside a code span. MDX parses {...} as a JSX expression; {{count}} compiles (an object literal) but throws "count is not defined" at render, so every Vercel multica-docs deployment since e3bf9cc fails. CI never caught it because turbo runs with --filter='!@multica/docs'. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…L-5722) (multica-ai#6420) Layer 1 (multica-ai#6383) raised the shared scanner cap to 32 MiB, which moved the cliff without removing it — a codex rollout is append-only, so a thread that outgrows any fixed cap still fails its resume forever. This adds the recovery path. Layer 2: an oversized thread/resume response is reported as Result.ResumeRejected rather than a crash, which is the positive evidence shouldRetryWithFreshSession needs and reconnects the recovery multica-ai#5715 had unintentionally cut off. Reaching it required one real fix — the reader wrapped the scanner error with %v, so bufio.ErrTooLong never survived to a caller. Layer 3: a new codex_resume_oversized reason marks the session resume-unsafe, and both resume lookups block by TIME rather than by matching the failed row. That shape is required, not stylistic: the failure happens before the turn starts, so the row lands with session_id NULL and is dropped by latest_per_session before any error-text filter runs. The block expires once a thread terminates after the overflow, so an issue recovers instead of starting cold forever. Also splits the MUL-4424 continuity notice by what each surface can still read — issue comments and Slack channel history can be re-read, web chat and Feishu cannot — so only the last group tells the user a loss happened. The backend no longer holds any of that wording; it receives ExecOptions.ResumeContinuityNotice from the caller and stays silent when the prompt already carries it, which makes the duplicate injection on the retry path structurally impossible. Known gap, tracked not claimed: for chat, the claim handler reads chat_session.session_id before the fallback query, so a daemon predating this PR leaves that pointer naming the oversized thread. Clearing it needs the attempted session recorded at claim time, which is a schema change.
* fix(chat): make queue action mutually exclusive (MUL-5760) Co-authored-by: multica-agent <github@multica.ai> * fix(chat): align queue UI with desktop reference (MUL-5760) Co-authored-by: multica-agent <github@multica.ai> * fix(chat): use solid queue icon tone (MUL-5760) Co-authored-by: multica-agent <github@multica.ai> * fix(chat): preserve stop during uploads (MUL-5760) Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address queue review nits (MUL-5760) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…#6426) * feat(issues): add header thread navigator to issue detail The right-edge rail is a good position indicator and a bad finder: a tick carries no text, so locating a thread costs one hover per candidate, and once threads outgrow the rail the ticks compress to their 5px floor and stop being countable. Add a header entry point that lists the threads instead — 8-10 titles readable at once, plus search, resolution/@me filters, and day grouping, so 24 threads and 240 threads cost the same to navigate. Hover previews the list without taking focus; a press pins it, focuses search, and enables arrow-key navigation. Pure hover would have been wrong for a surface you must scroll, type into, and arrow through — all three need the pointer free to leave the button. The two navigators stay one coordinate system rather than two competing lists: both derive the thread set and "you are here" from one memo and one shared useVisibleThreadIds, and hovering a row lights that thread's tick on the rail. Adds Mod+Shift+O (openThreadNav), mirroring "go to symbol in file". MUL-5755 Co-authored-by: multica-agent <github@multica.ai> * feat(issues): tint search matches in the thread navigator Filtering told the reader which threads matched but not why: a row that matched on its excerpt looked identical to one that matched on its title, so they had to re-find the term they had just typed. Tint the matches in both fields using the same --find-match token as the in-page find bar. Caught by driving the real app rather than the unit tests — the mock had this and the implementation had quietly dropped it. MUL-5755 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): drop the viewport marker and use real keycaps in the thread panel Two things the panel got wrong, both visible only once it had real data. The "you are here" marker came from the same viewport set the rail uses to darken its ticks, but "on screen" is a set, not a point: a tall viewport holds several threads at once, so the marker landed on two or three rows and read as a broken multi-select. Absolute position is the rail's job, where a column of ticks can show a span honestly; the panel's job is finding. Removing it also removes the panel's scroll-container dependency, so useVisibleThreadIds goes back to being the rail's private hook. The footer hints were hand-rolled kbd elements with arrows typed into the translation strings, so they matched nothing else in the product. Use the shared ShortcutKeycaps, which draws the same keycaps as the sidebar, command palette, and confirm dialogs, with the platform's own glyphs. The trigger also gains a tooltip like its neighbours, carrying the Mod+Shift+O keycaps — until now the shortcut had nowhere to be discovered. MUL-5755 Co-authored-by: multica-agent <github@multica.ai> * feat(issues): accept the command bar's Ctrl nav aliases in the thread panel Ctrl+N/J move down and Ctrl+P/K move up, matching the command bar — cmdk enables `vimBindings` by default and switches on exactly those four chords, so the palette has always accepted them and the thread navigator was the odd one out. The policy already existed as pickerNavigationDirection, added for the editor's mention and slash pickers for this same reason (MUL-5495). It lived in suggestion-popup.tsx, which imports Tiptap and floating-ui, so reaching it from a header popover would have dragged the editor's dependency graph along. Moved both it and isPickerAcceptKey — pure KeyboardEvent predicates either way — to common/picker-keys.ts, with their tests, and repointed the three consumers. preventDefault matters for the letter aliases here specifically: focus sits in the panel's search field, where Ctrl+K/N/P are readline editing commands that would otherwise mangle the query while moving the cursor. Tab is deliberately not an accept key in this panel, unlike the editor pickers, because Tab has to stay focus navigation for the filter pills. MUL-5755 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): four keyboard and data correctness bugs in the thread panel From review on multica-ai#6426. All four reproduce; three were mine to begin with. IME: handleKeyDown acted on every Enter, so a CJK user committing an IME candidate in the search field jumped to the active row and closed the panel mid-word. Guard with isImeComposing, the same check Cmd+F already uses. Child controls: the handler sits on the popup so navigation works anywhere inside it, which also caught Enter bubbling from the filter pills and the rows — and preventDefault on a button's keydown cancels the click the browser was about to synthesize. Enter on "Resolved" jumped away instead of filtering. Handle Enter only when the search field is the target; buttons keep native activation. The existing keyboard tests fired at the popup root, which no real interaction targets, so they could not have caught this; they now fire at the search field. Legacy mentions: mentionsUser matched only mention://member/<id>, and its comment claimed the old [@ id=... label=...] shortcodes were normalised before storage. They are not — preprocessMarkdown migrates them on READ, and the timeline hands us raw content, so "@me" silently dropped threads whose only mention of the reader used the old format. Run the same normaliser first; it returns its input untouched when there is no shortcode. Timestamps: formatClock used "less than 48 hours ago" while threadDayGroup used calendar days. The two disagree for most of the day, so an entry from 23:41 the day before yesterday grouped under "Earlier" but rendered as a bare "11:41 PM" with nothing saying which day. Take the group as an argument — one boundary cannot drift from itself. MUL-5755 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): one keyboard cursor in the thread panel, and a live shortcut hint From the second review pass on multica-ai#6426. Focus and highlight could drift. Gating Enter to the search field last round left the arrows firing for the whole popup, so Tab to a row, ArrowDown, Enter moved the highlight to one thread and jumped to another — DOM focus and the highlight were two pieces of state with nothing holding them together. Rather than gate the arrows too and leave the rows as unreachable-but-focusable tab stops, make the relationship structural: rows are role=option with tabIndex=-1, the list is a listbox, the search field is a combobox that names the current row through aria-activedescendant. Focus never leaves the search field for the list, so there is only one cursor to disagree with. Tab still reaches the filter pills, which keep native Enter activation. This also fixes rows having outline-none with no focus-visible replacement: they are no longer keyboard-focusable at all. The shortcut hint used getShortcut(), a getState() snapshot with no subscription, under a comment claiming it would pick up a rebind. It would not: rebinding openThreadNav in Settings left the tooltip showing the old key while the keydown handler already used the new one. useShortcut subscribes, and is what chat-fab already uses. MUL-5755 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#6441) * docs(changelog): add v0.4.19 release entry (2026-08-05) (MUL-5766) Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): retitle the v0.4.19 entry per maintainer wording (MUL-5766) Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): cover the Issue header thread navigator (multica-ai#6426) (MUL-5766) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…multica-ai#6444) * fix(chat): decouple the follow-up queue card from the composer chrome The queue used to restyle the composer on appearance (hasQueue prop flipping rounded-lg to rounded-4xl plus a stronger shadow), which made the input box visibly jump the moment a message was queued. The stack also relied on a three-way implicit contract (-mb-8 / pb-10 / z-10) spread across both components. Now the composer chrome is constant — no hasQueue prop, no conditional classes, only a static z-10 so it always paints on top — and the tucked look is owned entirely by ChatQueue: it slides its own bottom edge under the composer (z-0, -mb-3, pb-4) and insets itself (mx-3) so the stack reads as two distinct layers. The queue chrome itself is lighter and smaller: caption-size muted rows, xs action buttons, border-only card, and an animate-in entrance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(chat): align queue scroller cap assertion with max-h-40 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
multica-ai#6440) * feat(issues): show per-run token usage on the execution log (MUL-5762) The execution log already lists every agent run on an issue; it just never said what any of them cost. task_usage has held per-task token counts since migration 032 — nothing surfaced them per run. Three placements, one data source: - Execution-log header carries the issue total ("2.1M · $4.92") and opens the breakdown. - Each row carries its own token figure. This takes the slot the relative timestamp held: the sidebar is 288px and a third column would come out of the trigger text, which is what people scan. The list is sorted newest-first, so ordinal recency is already free; the timestamp moves into the row tooltip alongside duration and model, neither of which was surfaced there before. - The transcript dialog gets the same figure in its header, with the input/output/cache split in the run-info popover. Backend: ListIssueTaskUsage returns per-(task, provider, model) rows in one query, joined onto the existing task-runs response. The model dimension stays on the wire because cost is priced client-side per model — a row that collapsed two models cannot be priced at all. Cost reuses estimateCost from the runtime usage page, so the issue and the workspace never disagree; the new summarizeTaskUsage helpers live next to it rather than starting a second cost formula. No usage recorded stays distinguishable from zero end to end — omitted on the wire, undefined in the schema, null from the summarizer, an em dash in the UI. A run from before usage reporting was not free. Removes the standalone "Token usage" sidebar section: it showed the same issue totals minus the cost and minus any way to attribute them, and every field it had is in the dialog. The /api/issues/:id/usage endpoint it read stays — the CLI's `issue usage` still uses it. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address review on per-run token usage (MUL-5762) Three findings from @emacs, all confirmed against the code: 1. Drop the token figure from active rows. The daemon reports usage once, after `runner.run` returns (internal/daemon/daemon.go), and ReportTaskUsage publishes no realtime event — so a running task has no usage to show and would not learn of it mid-run if it did. The branch was only ever exercised by a hand-written fixture, which is a test asserting a scenario production cannot produce. The row keeps its timer; restore the figure in the same change that adds incremental reporting + cache invalidation. 2. Subscribe the usage surfaces to the custom-pricing store. estimateCost reads custom rates imperatively via getCustomPricing(), so nothing re-rendered these after a saved rate change — the header total, the dialog's totals and per-run costs, the cost-by-agent split, and the "unmapped model" notice all kept quoting the old price until the task list happened to refetch. Same subscription the runtime usage page already carries, plus the snapshot in every memo that prices usage. Regression test pinned: it fails without the subscription. 3. Give the dialog's status glyph an sr-only label. TaskStatusIcon is aria-hidden, so a screen reader could not tell a failed run from a completed one — the execution log rows already pair the two. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…762) (multica-ai#6450) On a real issue the breakdown painted outside its own box: the KPI cards, the by-agent bars and the whole run table rendered over the page behind the dialog, and the Cost column — the one people open this for — was unreachable. DialogContent is a grid and the scroll container was a plain flex item, so both defaulted to `min-width: auto` and sized themselves to the table's min-content width instead of to the track. `overflow-auto` then had nothing to clip: the box grew to 1032px inside an 896px dialog and every sibling stretched with it. `min-w-0` on the content column and on the scroller is what lets the table scroll instead. Two things made the table wide enough to trigger it: - The Model cell is uncapped, and a run that spilled across models prints both ids in full (`claude-haiku-4-5-20251001, claude-opus-5[1m]`). One such run set the width for the whole table. Capped, truncated, full list in the title. - Nine columns plus the token bar need ~920px, which 4xl could not give them. 5xl fits the table with no scrolling at ≥1100px viewports; below that it scrolls, which is the honest outcome for a dialog that cannot be wider than the window. Also floors the cache hit rate. 99.55% was rounding to "100% hit rate", which asserts every token came from cache on an issue that plainly read fresh input; flooring only ever prints 100% when it is actually 100%. Verified in a browser at 1440 / 1100 / 900 — nothing paints outside the dialog at any of them. jsdom has no layout engine, so the tests pin the contract that produced the bug (the scroller must be able to shrink) plus the truncation, hit-rate and a11y behaviours; all fail without their fix. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…hanges (MUL-3269) A daemon whose `multica` binary or agent CLI was replaced out of band kept running the old version until someone restarted it by hand, and most people never knew they had to. Two separate behaviors, deliberately not one: - The `multica` binary being replaced (brew upgrade, a manual download, a downgrade) is followed by a restart, once the daemon is idle. A running task is never interrupted; a busy daemon defers to the next check and reports the reason through `daemon status` / `reload_pending_reason`. This is independent of the GitHub auto-update poller and has its own switch (`--no-auto-reload` / `MULTICA_DAEMON_AUTO_RELOAD` / `disable_auto_reload`), because "don't pull new versions" and "follow the binary I replaced myself" are different intents. Desktop-managed daemons stay excluded. - An agent CLI upgrading in place is a hot refresh: re-probe, refresh the cached version and the server-side registration, and let subsequent tasks run under the new version's policy. Multica's availability does not track a third party's release cadence. Failure semantics are explicit. An unreadable, blank, or unparseable version is "no evidence", not a version change: the runtime and last trusted version are kept and the next round retries. A version confirmed below the minimum takes that provider's runtimes offline once the daemon is idle, and recovers automatically on upgrade. A late register response, a newly synced workspace, or an older cleanup request can neither revive a runtime already judged too old nor knock out one that has legitimately recovered. No migrations, no server endpoints, no frontend changes.
…ultica-ai#6447) Setting a reasoning effort on a Hermes agent failed with "thinking_level \"high\" is not a recognised value for runtime \"hermes\"", which reads like a typo. It is not: Hermes has no reasoning control on the surface Multica drives it over, so no spelling of the value can ever work. Add ThinkingControlSupported as the capability predicate behind the existing token gate, and use it so the API answers with the capability gap instead of blaming the value. The Hermes evidence (ACP session/new advertises no configOptions, set_config_option is inert, _make_agent never sets reasoning_config) is recorded next to the predicate so the next reader does not re-derive it, with a pointer from hermes.go. No behaviour change to which values are accepted. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…L-5442) (multica-ai#6425) Stage 2 section 2. The delivery contract keeps every platform fact and boundary: comment-add as the only delivery channel, the invisible terminal, exactly one comment per run before turn exit, --attachment as the only file path, the runtime-local-path ban with the code-location form and the say-so-in-words fallback. What goes is derivation: the invisible-task consequence restatement, the plans-in-your-reasoning elaboration, the good/bad style examples, the exists-right-now clause, and two quick-create explanation tails. One pin re-anchor: 'Do not assume any workspace issue prefix' follows the rewrite to 'never assume a workspace issue prefix'. Every other Output and delivery-invariant pin passes unchanged, including the MUL-4899 trio and the anti-dangling pointer target the Attachments section names. Issue-kind Output: 1,493 -> 978. Brief (real-UUID fixture): 13,382 -> 12,907 (-475); quick-create variant sheds ~160 more on its own kind. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
…5786) (multica-ai#6464) The panel was denser than any other list surface in the product and its bands did not agree on where content starts: the search icon sat at 10px, the filter pill label at 18px, the day label and rows at 12px. Three rails a few pixels apart read as a mistake rather than as structure, and three dividers in a 380px popover made it read as stacked bands instead of one surface. Everything now insets to 14px — px-3.5 on the search and footer, 6 + 8 on the pill row and the list — and the divider between search and filters is gone, leaving one header block. Vertical rhythm follows: search 36 -> 44px, pills 24 -> 28px, rows py-1.5 -> py-2, footer 32 -> 36px, and the day label now hugs the group under it. Width goes 380 -> 400px so the wider inset costs no excerpt width. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
2 times, most recently
from
August 6, 2026 04:33
5460cb2 to
0eee250
Compare
…rewrite (MUL-5442) (multica-ai#6453) * refactor(daemon): rewrite Mentions and Comment Formatting to their judgment form (MUL-5442) Stage 2, final two sections, bundled per the small-sections rule. Mentions: the four-link side-effect table stays verbatim (platform facts); the two H3 subsections merge into one policy paragraph keeping every anti-loop anchor — the no-mention default with its cost mechanism, the no-sign-off-mention ban, the end-with-no-mention rule, the three mention-warranted cases, and the silence closer. The retired headings' pins re-anchor to the policy phrases. Comment Formatting: both variants keep the full operational contract (file-first sentence verbatim, both bans with incident ids, workdir scope, --parent continuity, cleanup, newline rule); what goes is the mechanism narration (what the shell rewrites, how flags get swallowed, PowerShell version/encoding detail — the consequence stays). Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): correct the PowerShell version claim, pin the mention scope qualifiers, section-scope the formatting assertions (MUL-5442) Review catches by Elon on multica-ai#6453, all three accepted: 1. The compressed Windows rationale over-generalized a version-specific fact: '$OutputEncoding drops non-ASCII' is true of Windows PowerShell 5.1 (ASCII default), false of PowerShell 6+ (utf8NoBOM). Now reads 'Windows PowerShell 5.1 ... may replace non-ASCII characters with ?'; the Go comment documents the version split and why file-first stays version-agnostic (agents cannot rely on which shell services the pipe). 2. The merged Mentions paragraph was pinned only at the list head — the scope qualifiers ARE the anti-repeat-notify boundary: 'not yet involved', 'for the first time', 'explicitly asks to loop someone in', and the loop-cost mechanism are each pinned individually now. 3. The Comment Formatting assertions ran against the whole file, where 'multica-ai#4182' also appears in Available Commands — the HEREDOC ban could vanish with green tests. The assertions now slice the section (matched at the line-start heading, since Available Commands references the heading inline) and cover all seven contract elements within it. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
…7) (multica-ai#6465) PR multica-ai#6415 took the issue-detail content column from `px-8` to `px-4` below `md`. On the phone it still reads as a wider margin than the column needs: 16px on a 393px screen is 8% of the width spent on nothing, and the column is a reading surface whose longest lines — code, identifiers, pasted URLs — are the first thing to wrap. 12px. Both gutter sites move together. The skeleton exists to hold the column's shape while the issue loads, so a gutter that lands on only one of them reflows the column sideways at the moment real content mounts — the exact thing multica-ai#6415 matched them to prevent. That match was a comment; it is now a test, since a follow-up touching one site is how it breaks. Vertical padding is unchanged, and nothing above `md` moves. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
from
August 6, 2026 05:11
0eee250 to
524ea94
Compare
…ultica-ai#6483) Both local probes ran every 5 minutes. Neither has caused a measured problem, but background work a user never asked for should justify its cadence, and 5 minutes was tighter than either needs. 10 minutes for both, and the remote GitHub poll stays at 6 hours. That split is the point: only the remote poll asks whether a new release exists, so only it should track release cadence. The two local probes ask whether the binary on this machine has already changed, which is bounded by how long a user is willing to wait after acting, not by how often we ship. Not longer than 10 for either. selfReloadCheckInterval compounds, because a tick landing on a busy daemon defers rather than interrupting a task, so the real wait is the first tick that is both due and idle. agentVersionRefreshInterval also gates the below-minimum verdict, so its interval is the window an unsupported CLI keeps claiming work — cost alone should not push it out. Tests override both vars, so none needed updating. Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#6486) `new_comment` and `mentioned` shared the `comments` preference group, so muting comment volume also silenced @-mentions — and because a muted item is never created, those mentions were lost rather than deferred. In agent-heavy workspaces the only lever that reduces inbox noise also removed the signal the inbox exists for. `mentioned` now maps to its own `mentions` group. Preferences are stored sparse (a missing key means "all"), so existing `{"comments":"muted"}` rows default the new group to "all" with no migration: muting comments is a volume decision, not a decision to become unreachable by name. The split also aligns the global setting with behavior the platform already had — per-issue unsubscribe keeps delivering direct mentions, and mention delivery bypasses the subscriber table entirely. Closes multica-ai#6468 Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…ltica-ai#6489) The issue sidebar is a resizable panel — 260px minimum, 320px default — and the execution-log header has carried three items since the issue total moved into it: the section label, the active-run count, and "24.6M · $31.18". At the 260px minimum the row has 227px and that form wants ~238px, so the label — the only item that could shrink — broke "Execution log" across two lines. A section heading that reflows mid-phrase reads as broken, so the label now never wraps: whitespace-nowrap plus an ellipsis backstop for a longer translation. That alone would only move the damage (a squeezed "Executio…" is no better), so the header also tiers on its own width via a container query, never the viewport's — two sidebars of different widths can be open in one window. Below the tier the issue total drops its token figure and keeps the cost. It is a figure that yields, never a figure's digits: a clipped "$31.1…" would read as a number the issue never spent, and the token split is one click away in the dialog the total opens. Two thresholds, because the row has two shapes — 16rem beside the active-run chip (~246px of content), 14rem without it (~218px) — so an issue at rest keeps its whole total at every width the panel can reach. Widths measured in Chromium against the real 12px caption step, across 260 / 272 / 280 / 288 / 320 / 420 panels, both header shapes, with wide-number fixtures ("2400M · $1234", a two-digit run count): no wrap, no clipped label, no overflow at any of them. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#6492) Two mobile-web adaptations driven by real-device feedback: Issue comments (<768px): card interior padding drops 16 -> 12px, the 40px avatar hanging indent on comment bodies goes away (GitHub-style full-width body under the header), and the page gutter tightens to 12px. Comment text width on iPhone SE grows 269 -> 325px (+21%). Floating chat: on small screens the floating card becomes a full-screen panel (Lark-style) — resize handles and the expand control disappear with it. A new useVisualViewportKeyboard hook reports the visual-viewport geometry while the soft keyboard is up; the panel pins its bottom to the visible edge and caps its height to the visible strip, so the composer rides the keyboard instead of hiding behind it. Key measurements baked into the hook: iOS Safari shrinks window.innerHeight with the keyboard (layout height must come from documentElement.clientHeight), and iOS pans can bring the layout bottom back on screen while the keyboard still eats half the height. motion.div never unsets style/animate keys that vanish between renders, so both keyboard branches write the same style keys and width/height stay motion-owned in both form factors. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): Mika issue-first onboarding
Replaces the starter-agent welcome with one conversation: onboarding creates
Mika, the workspace's built-in Chief of Staff, and opens a real chat whose
first turn is a product-authored kickoff hidden from the transcript. Every
workspace — first and subsequent — is created through this flow.
Mika is a system agent, not an agent-template instance. Her product prompt is
//go:embed-ed and composed at claim time, so a release updates it without
touching any workspace's row; the row holds only the workspace's own notes.
Creation is server-owned and idempotent under a per-workspace advisory lock,
and archiving a system agent is rejected.
This is the pre-merge half of the branch, squashed while rebasing onto main.
Replaying its fifteen commits individually meant re-deriving each one against
a main they were never written for; the net change reconciles against today's
main in four files, so it is reconciled once, here.
Three of those four are main moving under the branch: MUL-5573 took
quick-actions generation server-side and dropped QuickActionsDisabled /
RegenerateQuickActionsFor from the task payload and the SendDirectChatMessage
signature, so this takes main's shape and keeps only the onboarding entry
point. The fourth keeps main's OnboardingLogoutButton wrapper around the
flow's new mode/onCancel props.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): let quick-action chips carry the opening's examples
Chat now renders agent-suggested follow-up actions as buttons under a reply
(MUL-5149), and the onboarding kickoff qualifies for that suggestion pass
with no extra wiring — it is a direct chat turn on a web session with a
non-empty reply.
That made the opening's fourth beat redundant: Mika wrote a three-to-five
line menu of example tasks, then three chips appeared underneath offering
the same thing. The prose menu is the worse half — a member has to retype a
line they read, but can send a button — so the beat is gone and the opening
budget drops with it. Measured on a real local run: the first reply went
from 307 to 208 characters, and the chips still arrived 21s after the
kickoff.
The questionnaire profile stays in the kickoff. The suggestion pass resumes
the same provider session, so the profile now steers the chips as well as
the reply; only the sentence naming its purpose changes.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): cut the step rails down to what the screen can't already show
The right rail carried more than twice the words of the column members were
actually working in — 42 vs 36 on the workspace step, 91 vs 34 on the runtime
step — and it got heavier the further in the flow went, which is backwards.
Step 3's rail was the clearest case. Its "Good to know" section promised the
runtime was swappable and that more could be added later; the step's own lede
already said both in nine words rather than forty-two, on the same screen. Its
60-word definition of "agent runtime" sat beside a list of named, online
runtimes under a headline reading "This computer is connected" — by then it
answers a question the member has stopped asking. What survives is the one
thing the screen does not show: what that background process is.
Step 2's rail keeps the workspace preview card, which does show something not
otherwise visible, and drops the bullet lists — promises the product is about
to keep on its own.
The freed words did not move to the rail; one moved into the main column. On
the create path "Mika" was never introduced before Step 3 used the name twice,
once on the primary button, so the lede now names the role in an appositive
right above that button. Mika stays ungendered, as everywhere else in the
product.
Net across the three regions: 167 words to 89.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): drop the right rail from the workspace and runtime steps
Every remaining rail item was either something the screen already showed or
something the product was about to do anyway, so the column was costing a
member's attention without answering a question they had. Removing it leaves
each step a single full-width column — the shape the questionnaire step has
always had, and the only step nobody has complained reads as sparse.
Gone with it: RuntimeAsidePanel, the workspace preview card and its entity
rows, and 27 copy keys per locale. The two runtime paths (desktop
runtime-connect, web platform-fork) shared that panel, so both lose it in one
move and stay identical.
The welcome step keeps its column — it holds an illustration, not prose.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): put every step's header and content on one measured axis
Removing the right rail left the four steps geometrically inconsistent in
four ways, all of which the member sees as things moving while they advance.
The header carried the horizontal padding itself, so it ran flush to the
window edge while the content column stayed centred. A 480px rail had been
absorbing that difference; without it the two were 267px apart on a 1283px
window, and since StepHeader is justify-between the step indicator floated
off at the far right, ~270px from anything it labelled.
The rest compounded it: the measure changed between steps (920px on the
questionnaire, 620px after), the header bar and content block used different
vertical padding per step, and padding living inside a max-w box made the
reading width jump from 508px to 620px at the lg breakpoint.
All four now come from step-shell.tsx. Padding belongs to the gutter, never
to a measured box, so the reading width is constant from ~700px up. The
header measures on STEP_FRAME on every step, so the one element that survives
each transition never moves; content picks STEP_FRAME or STEP_COLUMN by what
it holds, and both centre, so a step that needs the width still sits on the
header's centreline. Vertical rhythm is one value.
The header was near-identical in four files, which is how it drifted in the
first place, so it is now one component. Its test pins the invariant that
broke — padding out of the measured box, header measured on the frame — and
fails if either is put back.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): give the runtime step the frame measure and cut its copy
Nine runtimes in a 620px column meant truncated names, five rows, and
scrolling to reach the last four, with ~330px of dead space on each side.
This is the case step-shell's wider measure exists for, so the step takes
STEP_FRAME — which also puts it on exactly the header's measure — and the
card grid gains a third column at lg. Nine runtimes now land in three rows.
Copy went with it. The headline was two sentences over two lines; the first
one, "This computer is connected", is already said louder by the "9 agent
runtimes · all online" row directly beneath it, so only the instruction
remains. The lede was 44 words and five lines — I had grown it myself adding
Mika's introduction — and is 20 now, still naming the role. The
remote-computer note drops from 25 words to 15.
Prose stays capped at 620px inside the wider frame; a 920px measure is for
the card grid, not for reading.
The found-phase test keyed on the headline copy, so a copy edit read as a
behaviour regression. It now asserts the runtime count row, which is the
signal the test is actually about.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): put the workspace step on the shared frame
It was the only step still measuring at STEP_COLUMN, so its eyebrow, headline
and footer CTA started and ended ~150px inside where every other step's did —
the page margins visibly moved when you advanced from step 1 to step 2.
The step now sits on STEP_FRAME like the other two, with a new STEP_MEASURE
capping the prose and the form inside it. Matching the frame is not the same
as widening the field: a workspace name does not want an 800px input, so the
form keeps its reading measure and left-aligns to the frame instead. The
footer row spans the frame, which is what puts the CTA in the same place on
all three screens.
STEP_MEASURE deliberately does not centre — centring would pull the content
off the frame's left edge, undoing the alignment. Its test pins that.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): close the gap above the step CTA and put Log out back on one row
Two defects that read as one thing on screen: a large empty block sitting
directly above the primary button.
The card list was capped at STEP_MEASURE along with the form. That cap exists
so a workspace name does not get an 800px input — a good reason for a text
field and a bad one for selection cards, which are a list like the runtime
grid. Capped, they stopped 299px short of the CTA, leaving a void above the
button. The cards take the frame now; only the form keeps the reading measure.
Log out came in from main as `fixed right-8 top-8`, pinned to the window
corner. Its own comment says the fixed position exists to survive the flow's
full-bleed layouts — which is what the measured frame replaced, so it landed
outside the measure and above Back / Step N of N as a second header row. It
now rides the header row on the frame.
StepShellHeader takes it as a `trailing` slot rather than rendering it:
calling useLogout inside the shared header forced a QueryClient into five
step test files just to render a header bar. The flow injects it, matching
how runtimeInstructions is already threaded, and the header stays
presentational.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): make step 3 about Mika, with the runtime as a sub-decision
The step was titled after its dependency — "Pick an agent runtime" — while
the thing actually being created was named only in the grey lede. A member
reached a button reading "Start with Mika" without having been told who that
is, and said so: being confused there is not a failure to read carefully, it
is the page putting the lead role in a footnote.
So the subject changes rather than the step count. The headline names the
outcome, a card carries the introduction with a mark on it, and the runtime
list drops to a labelled sub-section under "Where should Mika run?". Reading
order becomes: who you are getting, she needs a machine, pick one, start.
MikaIntro sits above the phase switch so the subject holds still while the
runtime block below cycles through scanning / found / empty, and each phase's
own heading drops from h1 to h2 now that the page has a real h1.
Mika does not exist yet at this point — she is created on commit — so the
card cannot render her stored avatar. It reuses the mark the Runtimes page
already uses for "Start with Mika", so the two entry points read as the same
thing.
No fourth screen: the introduction and the only decision on this screen are
one beat, and splitting them would add a step between finishing setup and the
payoff. The defect was never a missing screen, it was an invisible
introduction.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): pick the runtime and the model from dropdowns
Nine runtimes as cards took three rows and left nowhere to put a second
decision. Two dropdowns fit both on one screen, and the model is a real
choice the card grid had no room for.
Both controls already existed on the agents surface — RuntimePicker and
ModelDropdown, the same pair create-agent-dialog uses — so this reuses them
rather than growing a parallel picker. ModelDropdown owns its own discovery,
grouping and unsupported-runtime states, and selecting a different runtime
clears the model because models are per-runtime.
The model now reaches the agent: POST /api/agents/mika takes an optional
model, CreateSystemUserAgent writes it to the column the agent table already
had, and empty still means "whatever the runtime defaults to" — which is what
every deployment without per-agent model support gets anyway.
Also drops the "No local runtime, or prefer a remote computer?" note. It said
in twenty-five words what the Skip button next to it says by existing, and it
appeared on all three phases.
currentUserId comes in as a prop rather than from the auth store: reading the
store inside the step broke six tests that render it without one, the same
coupling the header slot avoided.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): stop gendering Mika in the intro card
Mika is ungendered everywhere else in the product — the agent instructions,
the onboarding skill and every other locale string avoid a pronoun. The intro
card I added last round reintroduced one in two languages ("she turns it into
an issue" / "她会把它变成一个 issue"), which the Chinese screenshot made
obvious. All four locales now read around it.
Co-authored-by: multica-agent <github@multica.ai>
* feat(ui): port the ReUI stepper primitive
Groundwork for rebuilding onboarding on the @reui/onboarding-3 interaction
model: a persistent vertical stepper with named steps and click-to-return
navigation, which our horizontal "Step N of 3" dots cannot express.
Routed to components/ui rather than the vendor's components/reui namespace —
it is our code now — and rewritten to the role-named type scale (text-xs ->
text-caption, text-sm -> text-label / text-caption, dropping the leading-none
the token already supplies). No other convention fixes were needed: shadcn had
already rewritten the imports, "use client" survived because main's
components.json now sets rsc: true, and it pulls no npm dependency we do not
already have (@base-ui/react is declared).
The block's other twelve registry dependencies are primitives we already own,
so only this one was installed.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): rebuild the step shell as a named progress rail
Onboarding's chrome was a row of dots and a "Step 2 of 3" counter. It told
a member how much was left but never what was coming, so every step arrived
unannounced -- the same reason the runtime step read as a surprise even after
it was retitled "Meet Mika". Naming all three steps up front makes onboarding
legible as a whole before the first field is filled in.
StepShellHeader becomes StepShell, and it owns the window rather than a strip
at the top: rail on the left, the step's own content scrolling on the right.
That lets the four steps drop an identical wrapper/DragStrip/header/<main>
preamble, including the scroll-fade wiring that was duplicated verbatim in
all four and is now set up once.
The rail is built on the ported ReUI stepper, but deliberately not as a
tablist: that component defaults to role=tablist with each trigger owning
aria-controls on a panel id, which is right for a stepper that renders its
own panels and wrong here, where the panel is a routed step and those ids
would dangle. It uses the presentational slots and marks position with
aria-current instead.
Only completed steps are clickable -- moving forward has to run the current
step's validation and submit, so the rail would skip it. New-workspace mode
gets no rail navigation at all: it enters at the workspace step and, once
that workspace exists, every step behind it is gone. Same invariant
runtimeStepBack already enforced for the Back button.
step_header.step_of is replaced by step_nav across all four locales; Mika
stays ungendered in each.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): standardise the steps on the shared UI primitives
Follow-up to the rail, fixing what the rail exposed.
The workspace form was three hand-rolled `flex flex-col gap-1.5` stacks with
their own label sizing and a bare <p> for the slug error -- exactly what
Field/FieldLabel/FieldError/FieldDescription standardise. The manual version
had already drifted: its labels were caption-sized and muted while every
other form in the product labels at body weight.
The platform fork wrapped on STEP_COLUMN while the steps before it wrapped
on STEP_FRAME. Both measures centre, so 620px and 920px put their left edges
~150px apart and the headline visibly jumped right on arrival. It now sits on
the frame and caps its own content, which is the pattern step-shell already
documents.
The About you eyebrow read "About you" -- now the rail's label for that very
step, so the page said its own name twice, once in grey caps and once in the
headline under it. Dropped, along with the locale key. The other steps keep
theirs because they say something the rail doesn't ("Connect a computer",
"Workspace creation is disabled").
Log out is `inline` on every step, and inline now means "on the rail", which
is an inverted surface -- the muted/destructive pair it used unqualified is
mixed from the light palette, so it was rendering as near-invisible grey on
black.
e2e: the smoke spec asserted "Step 1 of 3", text the rail replaced. It now
asserts the rail's named steps and which one is aria-current, and carries on
into the runtime step so all three get captured. Two pre-existing bugs in
that spec surfaced while fixing it: the zh-Hans case pinned its locale cookie
to a hardcoded port, and it advanced with getByRole("button").first(), which
is the pinned Log out button -- so that case had been signing the user out
and asserting against a login redirect.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): rebuild the rail as the ReUI inset panel
The first pass kept the block's idea -- a named vertical rail -- and dropped
most of its structure. Side by side with onboarding-3 the gap was obvious:
no brand lockup, no inset panel, no texture, steps jammed against the top
edge, numbered chips instead of the ring/check/dot progression, stub
separators instead of one continuous track, and a bare Log out where the
block has a footer row.
This ports the structure properly:
- Inset panel -- the aside carries the padding and the panel is rounded with
a hairline ring, instead of a dark rectangle bleeding to the window edge.
- Brand lockup top-left (the existing MulticaIcon plus a wordmark), Back
demoted to an icon button top-right, which is where the block puts it.
- Step list centred in the remaining height rather than stacked under Back.
- Indicators follow the block: filled + check when done, ring + dot when
current, faint ring when upcoming. Numbers move to sr-only text, since the
ring already encodes position and the digit was redundant next to a label.
- One continuous hairline behind each row instead of a stub between rows.
- DotSphere ported to packages/ui as the panel's texture. It is decorative,
reads no product state, and already honours prefers-reduced-motion.
Two fixes fell out of doing it properly:
`.dark` is a plain class selector in our token sheet, so scoping it to the
panel redefines the custom properties for that subtree and `bg-background` /
`text-muted-foreground` mean the right thing inside it. That replaces the
hand-mixed `text-background/60` shades of the first pass -- which is what had
made Log out near-invisible -- and it is why the button is back on ordinary
tokens here.
StepperSeparator hardcodes a 3rem height for vertical navs, so an absolutely
positioned track overshot its row and drew the line straight through the next
indicator. Overridden with the same variant rather than !important, so
tailwind-merge drops theirs.
DotSphere cycles three constant arrays by `index % length`. Under
noUncheckedIndexedAccess that is `T | undefined`, so they are typed as
non-empty tuples and index 0 is the fallback -- no non-null assertions.
Co-authored-by: multica-agent <github@multica.ai>
* feat(onboarding): put the content pane on the block's type and layout
The rail matched onboarding-3 and the pane it sat next to did not, so the
two halves read as different products: serif display headlines and a grey
uppercase eyebrow on the right, the block's sans hierarchy on the left.
Typography now maps onto the block exactly, and it lands on our scale
without rounding -- ReUI's `text-xl/7` heading is our `text-title-lg`
(20/28) and its `text-sm/5` supporting line is our `text-body` (14/20).
StepHeading owns both, so no step hand-rolls a headline again.
The eyebrows are gone. The block has no such slot, and with the rail naming
every step, a grey uppercase label above a headline saying the same thing
was the third name for one screen. The workspace step's disabled-state
wording was the only eyebrow carrying information the headline lacked, and
that variant already exists as its own headline copy.
Geometry collapses from three competing measures -- a 920px frame, a 620px
column, an in-frame cap -- to one 28rem column. Three measures is what let
the platform fork sit ~150px right of every other step. STEP_MEASURE stays
for capping a single control inside the column.
Actions move into StepFooter: full-width, stacked, pinned to the bottom of
the column. In a 28rem column the old right-aligned inline bar left the
primary action floating mid-screen instead of where the eye finishes the
form. The column is `min-h-full` rather than centred by the pane, because
`items-center` on a scroll container clips the top of anything taller than
the viewport and these steps do overflow on short windows.
Questionnaire options become the block's wrapping chips. They were
full-width cards in a 4-column grid; inside a 28rem column that grid had
nowhere to go, and stacking all 18 as rows turned one screen into a long
scroll. Chips are the block's own answer for a many-option question. This
also reaches the workspace source-backfill prompt, which shares the
component -- intentionally, it is the same question in the same style.
MikaIntro moves onto StepHeading + Item for the same reason.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): drop the rail clear of the macOS traffic lights
The rail is a dark inset panel and it started at the window's top-left
corner, which is exactly where macOS draws the traffic lights -- so the
close/minimise/zoom buttons sat on top of the dark surface.
The underlying mistake was structural. The desktop shell rule asks for a
single DragStrip as the first flex child of a full-window view; this had two
hand-rolled strips instead, one inside each pane, and neither was first. The
sidebar's was 28px of internal padding trying to duck under the traffic
lights from inside a panel that had already begun above them, which cannot
work -- the panel's own background was the thing being overlapped.
One DragStrip now spans the window above both panes. The panel starts below
it (48px strip + the aside's inset, measured at 64px on a wide window against
traffic lights that end around 32px), the whole band stays draggable, and the
two ad-hoc strips are gone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): make every block in the column share one width
Reported on the workspace step: the name field ended short of the
description above it. Measured rather than eyeballed -- the heading,
description and footer all render at 448px, the form at 384px, because the
FieldGroup still carried STEP_MEASURE.
STEP_MEASURE made sense against the old 920px frame, where a full-width
input would have been absurd. Against a 28rem column it does nothing but
misalign, so both remaining uses are gone -- the workspace form and the
runtime picker -- and the constant with them. A single measure that no step
can locally narrow is the whole point of the column; leaving the knob
exported invites the same drift back.
Two stale measures went with it. The runtime phase views still capped their
ledes at max-w-[620px], inert inside a 448px column, and sized them
text-body-lg against StepHeading's text-body; their h2 was text-title-lg,
the same size as the h1 above it. Both now match the shared scale.
Guarded in e2e rather than a unit test. The shell test renders a stub child,
so asserting "no narrow cap inside the column" there would pass whatever the
real steps do. The new spec walks all three steps and compares rendered
geometry -- it fails on the reported bug and passes after the fix, which is
what makes it worth keeping.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): stop the whole window re-fading on every step change
Every step rendered its own <StepShell>. Because each step is a different
component type, React tore the shell down and built a new one on each
transition: the "persistent" rail remounted, its canvas restarted, and the
shell replayed `animate-onboarding-enter` -- a 0.4s fade from opacity 0
across the entire window. That full-window re-fade is the flash.
Measured before changing anything: tagging the live <aside> and switching
steps showed the attribute gone, and the shell root still reporting
animation-name `onboarding-enter` after the switch.
The upstream block has no step-change motion at all -- no animate-*, no
transitions, no framer-motion, no AnimatePresence. Its sidebar and section
live in one component and only the step body swaps. This does the same: the
flow owns a single StepShell and the steps render content. The shell's
entrance fade now runs once, on entering onboarding, which is what it was
for.
`backDisabled` was the one thing blocking the hoist -- the shell needs it but
only the workspace step knows the create request is in flight. It reports
upward through `onBusyChange`, with an unmount cleanup: a successful create
advances immediately, so without clearing the flag the next step would open
with Back and the rail dead.
Guarded in e2e by tagging the rail and content nodes and asserting they
survive a step change, plus a single DotSphere canvas rather than one per
visited step.
Co-authored-by: multica-agent <github@multica.ai>
* chore: drop a stray commit-message file
Co-authored-by: multica-agent <github@multica.ai>
* i18n(zh): retranslate onboarding and fix cross-file inconsistencies
Reported as "生硬" on the onboarding Chinese. Reviewed all 217 onboarding
strings against the Chinese voice guide in conventions.mdx, then swept the
other 24 locale files for the same classes of problem.
Three were mistranslations, not stiffness. The worst: an agent runtime was
described as the AI coding tool "我们接管的" — take over — where the English
says "we connect to". Also a "推荐" that exists in no source string, and
"12 calls" (user interviews) rendered as "12 通电话".
Punctuation, decided from the repo's own zh docs rather than guessed:
「」 is forbidden by the guide and appeared 16 times; 破折号 spacing was
split 26 spaced vs the rest unspaced, and the docs run 152 unspaced to 35,
so unspaced wins. Four strings had a stray space inside Chinese text
("正在跳转到 工作区").
67 English strings had two or more Chinese translations. Most are
legitimate — weekday pickers use single characters where labels spell them
out, 飞书/Lark are deliberate regional variants, and "Name" is 姓名 for a
person and 名称 for an object. 42 were arbitrary and are now unified.
ja/ko got the same consistency pass on the clear-cut cases only, kept
parallel with the zh choices. Their punctuation was deliberately left alone:
「」 is standard Japanese quoting, so the zh rule does not transfer.
Edits are applied to the raw file text rather than through a JSON
round-trip, which reformatted compact one-line objects and turned ~20 real
edits into a 97-line diff.
Co-authored-by: multica-agent <github@multica.ai>
* chore: drop a stray commit-message file
Co-authored-by: multica-agent <github@multica.ai>
* feat(mika): use the unicorn emoji as Mika's placeholder avatar
Mika shipped with a hand-rolled data-URI SVG — a sparkle glyph on a dark
rounded square — which only that one constant knew how to produce. Agents
already have an emoji avatar convention (`emoji:` marker in avatar_url,
owned by agentEmojiAvatarPrefix on the server and parseAvatarEmoji on the
client), so this reuses it instead: ActorAvatar renders the emoji as text and
no surface needs to special-case her.
The two cards that stand in for Mika before the agent row exists move with
it — the onboarding intro card and the Runtimes "Start with Mika" card. Both
were drawing the same sparkle mark, and leaving them would mean a member sees
one face during onboarding and a different one the moment Mika is created.
They now share MIKA_PLACEHOLDER_EMOJI, which carries a pointer to the server
constant so the two cannot drift apart silently.
The dark square went with the sparkle: it was built to frame a white line-art
glyph, and an emoji on it reads badly. These use bg-muted, matching how
ActorAvatar already frames an emoji avatar.
Placeholder until Mika has real artwork.
Co-authored-by: multica-agent <github@multica.ai>
* i18n: stop the skip path promising things it does not do
Two claims in the runtime-skip copy that the code does not back, both found
while walking the flow end to end.
"Enter your workspace in read-only mode" — there is no workspace read-only
concept on the server. The only READ ONLY in handlers is Postgres transaction
isolation on the issue-table and search queries. What actually exists is
admission.go's ReasonAgentRuntimeRequired, which blocks dispatch when no
runtime is connected. So the sentence's second half was already true and
enforced; the first half promised a restriction that does not exist — a
member who skips can create issues, comment, edit fields and invite people
exactly as normal. Same over-claim in cloud_waitlist.intro_warning.
"We've added one task" — the skip path creates an *issue* (it lands on the
Issues board as NOR-1), and task/issue are distinct entities in this product.
Now says issue.
All four locales.
Co-authored-by: multica-agent <github@multica.ai>
* i18n: reframe Mika as your first agent teammate
Requested copy change on the step 3 headline.
The rail's description for that step moves with it. It is the step's own
subtitle and sits on the same screen as the headline, so leaving it saying
"Your Chief of Staff" would have put two different framings of Mika side by
side on one page.
Three "Chief of Staff" references are deliberately untouched, because
dropping the title everywhere is a positioning call rather than a copy fix:
the role chip on the intro card (name + title reads fine under the new
headline), and the web fork's lede, which is a different screen.
Chinese follows the glossary (Agent -> 智能体) rather than the mixed-language
phrasing in the request.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): let the member pick the runtime before Mika is created
"Start with Mika" provisioned immediately on
`runtimes.find(online) ?? runtimes[0]`. One machine commonly registers every
agent CLI installed on it — nine on the box this was reported from — so "the
first online one" is an arbitrary pick, and Mika could end up bound to a CLI
the member never intended to run their Chief of Staff on. Rebinding after the
fact is more work than choosing up front.
The action now opens a dialog with the same two controls onboarding already
uses for this decision, RuntimePicker and ModelDropdown, so the same choice
reached from a different entry point is asked the same way. The old heuristic
survives only as the dialog's initial selection, which makes it visible and
changeable instead of silent.
Model resets when the runtime changes, because models are per-runtime and a
value picked for the previous one may not exist on the next.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(runtimes): one component for "which runtime should Mika use"
Three surfaces asked this same question and had drifted apart: desktop
onboarding and the Runtimes page offered a runtime plus a model, while the
web CLI dialog offered only a runtime. `step-platform-fork` called
`onNext(picker.selected)` with no second argument, so connecting through the
web terminal path silently created Mika on whatever model the runtime
defaulted to, while the desktop step let you choose. Two of the three also
re-implemented "changing the runtime clears the model"; the third simply
lacked it.
MikaRuntimeChoice now owns the pair and that reset rule, so no caller can
forget it and the three entry points cannot drift again. The web CLI path
gains model selection, which is the behaviour change here.
`layout` is a prop rather than a single unified presentation because the
difference is real: the CLI dialog lists machines because that is the moment
they appear one at a time after `multica setup`, and a collapsed dropdown
hides exactly the feedback that dialog exists to give. Everything below the
list is identical.
compact-runtime-row moves from onboarding/ to runtimes/ so imports only flow
onboarding -> runtimes rather than both ways.
Creation is deliberately left alone. A and B still funnel through
`handleRuntimeNext`, which also runs saveQuestionnaire, completeOnboarding
and onComplete; the Runtimes page must not do any of that, since that member
is already onboarded. Merging those would leak onboarding completion into a
non-onboarding surface.
The platform-fork test now needs a QueryClientProvider, because the dialog
renders a model dropdown that queries the runtime's model list, and its
onNext assertion moves to the two-argument signature.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): stop the download card claiming a result it cannot know
Clicking "Use this computer" set `downloaded` unconditionally, which flipped
the card to "Opening the download page..." / "Opened in a new tab." Neither
is knowable: `window.open` is called with `noopener`, and per spec that
returns null whether the tab opened or a popup blocker ate it, so a blocked
click still produced a card asserting a tab had opened.
The same flag was also write-once, so the transient "Opening..." became a
terminal state — come back to the tab later and it still says the page is
opening. And because the swapped title wraps to two lines, the card grew
10px and pushed the two cards under it down, which is what made the click
read as a page refresh in the first report.
The card now states its intent up front — "Opens in a new tab — pick your
platform there" — which is true before the click, after it, and when the
popup never appears. The state, both `_after` strings and `hint_downloaded`
are gone from all four locales.
Its test asserted the flip, so it now asserts the opposite: the mocked
window.open returns null (the blocked case) and the card must be unchanged.
Measured after the change: primary card 330px before and after, and the
cards below it do not move.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): stop one worktree in a thousand booting into a blank window (multica-ai#6436)
Worktree renderer ports are `5174 + cksum(path) % 1000`, a 5174-6173 window
that contains exactly one port Chromium refuses to navigate to: 6000, the X11
port on its restricted list. A worktree whose path hashes to offset 826 gets a
healthy Vite server on 6000 and an Electron window that fails the load with
ERR_UNSAFE_PORT -- so it reads as a renderer bug, not a port one, and the only
way out was setting DESKTOP_RENDERER_PORT by hand.
Restricted ports in the window are now remapped into the block immediately
above it (6000 -> 6174). Sending them past the end rather than shifting them by
one keeps the offset -> port mapping injective, so two worktrees still cannot
land on the same port and race for it.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): close the Mika multi-member and failure-recovery gaps
From Emacs's review of multica-ai#6378. All seven findings reproduced.
The two blocking server bugs were the same shape: Mika is one agent per
workspace but sessions and ownership are per member.
- StartMikaOnboarding required the agent owner, so the member who lost
the CreateMikaAgent race — the race that handler's advisory lock exists
to survive — got a valid Mika, opened a valid session, and then a 403.
Mika is created workspace-visible and workspace-invocable; the session
gate and canInvokeAgent were already the checks that matter.
- The onboarding session was resolved client-side by listing sessions and
creating one on a miss, matched on the localized title.
LockWorkspaceForChatSessionCreate is FOR KEY SHARE precisely so
concurrent creators do not block, so two tabs each opened their own
conversation with its own kickoff, and switching language between a
failed attempt and its retry opened another. It is now get-or-create
server-side under a per-(workspace, member) advisory lock, keyed on
(workspace, creator, agent), returned alongside the agent.
Also:
- The skipped-runtime welcome dismissed itself silently when provisioning
the guide issue failed. The signal is not persisted and onboarding is
already complete, so a blip was terminal. It now offers a retry.
- The Runtimes recovery card gated on `agents.length === 0`, so creating
any ordinary agent hid the only surface that can mint a Mika — the
generic endpoint accepts no system_key. Gated on Mika's absence.
- CompactRuntimeRow ignored `disabled`; the CLI dialog was already
passing it, so the runtime could change mid-submit. It is a real
<button> now, which also gets focus and Enter/Space for free.
- The rail never went below 15rem while the content pane kept its gutter,
leaving ~87px of form at 375px. It is hidden under md, where a compact
bar carries the step name and the Back button instead.
- Dropped a stray __pycache__ artifact I had committed by accident.
Each new test was checked against the bug it covers: reinstating the
owner gate, the title-keyed lookup, or the dropped disabled prop makes
the corresponding test fail.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): make the Mika entrypoint survive a partial bootstrap
From Emacs's second review of multica-ai#6378.
Bootstrapping Mika is three server steps — provision the agent, open the
member's session, enqueue the opening turn — and the last two can fail
after the agent commits. Two things then conspired: the server reported
success with the session omitted, and the Runtimes card gated on the
agent existing. The agent's own `agent:created` broadcast invalidates the
agent list, so the card (and its open dialog) was torn down the instant
step one succeeded, and never came back on reload — the agent is durable,
the rest was not. The member was left holding a Mika they could not start.
- The endpoint now fails when the session cannot be resolved. Every step
is idempotent, so a retry converges; handing back a half-built flow the
caller cannot distinguish from a finished one does not.
- The entrypoint is gated on the member's own state — does this member
have a Mika conversation that was actually kicked off — rather than on
the workspace having an agent. That is the question the card answers,
and it is true again for every partial state above.
Also restores the Log out escape hatch below `md`. Hiding the rail last
round took its footer with it, which stranded every step but Welcome with
no way out on a narrow screen; the compact bar now renders the same slot,
so `sidebarFooter` is `chromeFooter`.
Each new test was checked against its bug: the old agent-only gate fails
three of the memberNeedsMikaSetup cases, and dropping the footer prop
fails the chrome test.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…nary (multica-ai#6488) handleModelList resolved the executable from d.agents()[rt.Provider], which holds built-in CLIs only, while runTask launches the custom runtime profile's own command (MUL-3284). A host with only the custom command installed answered the model list with `no agent configured for provider "hermes"`, so the picker stayed empty even though the same runtime ran tasks fine; a host with both installed enumerated one binary and executed the other, so the picker could advertise models the launched CLI rejects. Discovery now mirrors runTask's resolution order: the profile owns the path when the runtime is profile-backed, the built-in entry (with its MUL-4486 self-heal) otherwise, and the original failure is kept for the case it was written for. The 60s discovery memo carried the same defect — only codex / opencode / deveco keyed on the executable path, so a built-in runtime and a same-family profile runtime on one host shared an entry. Every dynamic-discovery branch now uses discoveryCacheKey; an empty path still maps to the bare provider key. Also stops pi's parser coining CLI usage text into model IDs: a pi-family profile pointing at a fork without --list-models printed a usage hint that the field splitter turned into a `Run/`omp` model. The filter is narrow enough that a real catalog row alongside a usage hint still parses, so reading a catalog off a non-zero exit (multica-ai#3729) is unchanged. fixed_args still does not participate in discovery — tracked in MUL-5807. Fixes multica-ai#6466. Fixes multica-ai#4482. Fixes MUL-5789. Fixes MUL-5471.
…5737) (multica-ai#6487) Desktop derives its daemon profile from the renderer-supplied API URL. Until that arrived, "no profile yet" was represented by an empty profile name, which resolved to the default profile owned by the user's terminal CLI. Three silent consequences: syncToken wrote token and server_url into ~/.multica/config.json; profileArgs omitted --profile so the bundled CLI ran against that same profile; and healthPortForProfile returned 19514 — the default profile's port — so a user's own CLI daemon could be probed and reported as Desktop's. resolveActiveProfile's docstring and desktop-app.mdx both already promised this could not happen. Nothing enforced it, and there was no test. Not Windows specific: none of the three vectors branches on platform. Replace the sentinel with null so the unresolved state carries neither a path nor a port, forcing all ten call sites to handle it, and move the path/port/arg helpers into daemon-profile.ts with a resolved-profile assertion as a backstop. Also fixes login ordering: syncToken's fail-closed branch had no recovery because the login effect only depends on the user while the URL was pushed by a separate effect. The sequence now lives in platform/daemon-login-sync.ts and awaits setTargetApiUrl before syncToken and autoStart. Refs multica-ai#6399
…auth (MUL-5803) (multica-ai#6482) * fix(agents): stop resuming a session that can't resolve its provider auth A Hermes agent on a self-hosted install can get permanently stuck on a single issue while every other issue on the same agent keeps working. The task terminates with: hermes provider error: "Could not resolve authentication method. Expected either api_key or auth_token to be set. Or for one of the X-Api-Key or Authorization headers to be explicitly omitted" and no amount of retry / Rerun recovers it. Root cause (resume-pointer poisoning): every daemon version classifies this text as agent_error.unknown, which is resume-safe. So GetLastTaskSession / GetLastChatTaskSession keep returning the same failed session to every retry and Rerun on that (issue, agent) pair, deterministically reproducing the auth error forever. Other issues use fresh sessions, so they're unaffected — the codebase's repeated "(agent, issue) permanently stuck" pattern (GH multica-ai#6066 / multica-ai#5760 / multica-ai#6360, MUL-5722), with this error falling through every prior defense. The fix rests entirely on text guards; the classifier is deliberately left untouched. Reclassifying this under missing_config would flip freshSessionMayHelp to false and silently disable the in-turn fresh-session retry on the five ResumeRejectionUndetectable backends — contradicting the (correct) diagnosis that a fresh session cures it: - service/task.go ResumeUnsafeFailure: text guard so manual Rerun and the fallback claim path start fresh rather than replaying the dead session. - GetLastTaskSession / GetLastChatTaskSession SQL: ILIKE exclusion so already-wedged issues recover on their next trigger without a daemon upgrade. Tests pin both halves: a freshSessionMayHelp regression (must stay true for this error), the Go ResumeUnsafeFailure cases, and SQL exclusion + narrowness regressions for both query families. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: address PR review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: qiushihao279-cloud <301943329+qiushihao279-cloud@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
…-ai#6497) Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
) Adds a DingTalk (钉钉) bot integration on the bring-your-own-app model: a workspace admin creates their own Stream-mode robot and pastes its AppKey / AppSecret, so no public webhook or OAuth redirect is required. Each agent gets its own bot identity, so several agents can be distinct, separately @-mentionable contacts in one DingTalk organization. Supports DMs, @-mentions in groups, inbound images, /issue quick-create, and /new. Built on the shared channel engine (ForceFresh/BareFresh, MediaResolver / MediaRef and the intent ledger) rather than a private implementation. Off unless MULTICA_DINGTALK_SECRET_KEY is set. Docs in en/zh/ja/ko. Closes multica-ai#4791. Community-maintained: @yyclaw is the code owner for server/internal/integrations/dingtalk/.
multica-ai#6495) kimi-code 0.33.0 exports no token counters over ACP, so every kimi task landed on the usage dashboard with no row at all. Read the counters from kimi's per-session wire log instead, the same fallback codex.go uses for Codex rollouts. The scan is bound to the ACP session id, buckets usage by the model each record names, covers subagent logs, filters records by timestamp so a resumed session is not re-billed, and sums only usage.record (the sibling step.end event repeats the same numbers). Verified end-to-end against the real CLI: the task reports input:5075 output:26 cacheRead:17664; with the fallback disabled the same run reports no usage at all. Fixes multica-ai#6448
…6499) We told @yyclaw the DingTalk integration would be documented as community-maintained. This makes good on that without turning the docs page into an ownership notice. - Docs (en/zh/ja/ko): one line under the intro — community-maintained, no official support SLA, where to report problems. Docs readers care about the support commitment, not who wrote it, so the maintainer's name is deliberately not here. - Package comment in server/internal/integrations/dingtalk: the full contract, including the code owner and the deprecation rule, aimed at the people who actually need it — anyone refactoring the shared channel engine. No behavior change. Co-authored-by: multica-agent <github@multica.ai>
Kimi exposes a per-model reasoning effort, but Multica had no way to set it: the agent form showed no thinking control for kimi, and a persisted value was ignored at execution time. Discover the per-model effort catalog by combining Kimi's ACP model list with `kimi provider list --json`. The provider catalog is the only authoritative source for per-model supportEfforts/defaultEffort: session/new's thinking option describes one newly-created session and can remain pinned to that session's original model after set_model. Apply a configured level before prompting via session/set_config_option. Both paths prefer degrading over failing. Discovery leaves Thinking nil — hiding the control while model selection keeps working — when the CLI is absent, the ACP response does not advertise the `thinking` config id, the provider list is unavailable, or its output does not parse. At execution time, a level that cannot be applied or confirmed logs a warning and the task continues at the runtime default, which is how every other provider already treats a configured effort. Co-authored-by: multica-agent <github@multica.ai>
bowser-bot
force-pushed
the
review/kimi-thinking-support
branch
from
August 6, 2026 07:53
524ea94 to
7c82e5f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Kimi runtimes currently do not expose model-specific thinking choices in agent settings. This change discovers the levels advertised for each Kimi model, exposes them through the existing model catalog, and applies an explicitly selected level through Kimi's ACP configuration before the prompt is sent.
The implementation reuses the existing per-model catalog and the daemon's provider-neutral validation path. It does not add a Kimi-specific validation branch in the daemon: unsupported saved values keep the established behavior of logging a warning, ignoring the value, and allowing the task to continue with the runtime's own setting.
Thinking path:
multica-ai/multica#6059identifies that Kimi agents have no Thinking control. The existingpkg/agentprovider dispatch already routes Kimi model discovery throughdiscoverKimiModels, and the shared daemon validation already consumes the supported levels returned by that catalog. The focused change therefore belongs inserver/pkg/agent; the daemon does not need a provider-specific code change.Related Issue
Related to
multica-ai/multica#6059. This is a review PR within a fork, so it deliberately does not use a closing keyword that would target the fork's issue namespace. The eventual upstream PR will useCloses #6059.Type of Change
Changes Made
server/pkg/agent/models.go: combine Kimi's ACP model catalog withkimi provider list --json, attach per-model thinking choices only when the ACP response exposes the exactthinkingconfig option, and leave models without advertised efforts without a Thinking control.server/pkg/agent/models_test.go: cover K3 effort discovery, models without efforts, alternate response field names, missing config identifiers, unavailable provider data, and parser validation.server/pkg/agent/kimi.go: apply a persisted explicit level withsession/set_config_optionand require Kimi to confirm the effective value before sending the prompt.server/pkg/agent/kimi_test.go: cover successful configuration, runtime errors, missing confirmation, mismatched confirmation, and prompt suppression when configuration is not confirmed.server/pkg/agent/thinking.go: allow well-formed Kimi effort tokens through the shared literal validation so the existing per-model daemon catalog can make the exact support decision.server/pkg/agent/thinking_test.go: cover Kimi's dynamic effort-token validation.How to Test
Run the repository's Go verification entry point:
make testThis was run with Go 1.26.1 against an isolated PostgreSQL database and passed, including the race-enabled Go suite.
With Kimi CLI 0.32.0 available, start Multica and its daemon, then create or edit an agent using a Kimi runtime and model
kimi-code/k3. Open agent settings and confirm the Thinking control contains Follow CLI config, Low, High, and Max.Select Max, save the agent, reload the page, and run a task. Confirm Max remains selected, Kimi confirms
thinking.currentValue=max, and the task completes.Select a
kimi-for-codingmodel and confirm that the Thinking control is absent because that model does not advertise effort choices.To verify compatibility with an existing saved value, first save Max on K3, then switch the model to
kimi-for-codingand run a task. Confirm the daemon logsthinking_level: not valid for this (provider, model); skipping injection, then the task continues and completes with the runtime's own setting.Risks and scope boundaries: A Kimi ACP session that is already being resumed does not clear a previously explicit thinking level when the agent is changed back to Follow CLI config. A new session correctly uses the CLI setting. Resetting an existing session, storing configuration provenance, and broader model-discovery reliability changes are intentionally outside this focused PR. This PR also contains no frontend, database, generated-code, or shared daemon changes.
Checklist
apps/web/features/landing/i18n/) and relevant docs (apps/docs/content/docs/)apps/docs/content/docs/developers/conventions.zh.mdx(terminology, mixed-rule fortask/issue/skill)The screenshot item remains unchecked until the images are embedded in the upstream PR. This change only updates internal implementation under
server/pkg/agent, so no user-facing documentation needed updating. The landing/docs and Chinese-copy items do not apply because this PR adds no runtime, coding tool, UI tab, or product copy.AI Disclosure
AI tool used: Multica Agent (based on Claude)
Prompt / approach: AI agents were used to implement the focused backend change, run independent and adversarial code reviews, expand automated test coverage, and verify the behavior in a real test deployment with Kimi CLI 0.32.0. Human feedback was used to narrow the scope and align the final change with the repository's existing provider behavior.
Screenshots (optional)
Sanitized screenshots have completed internal review: the K3 Thinking menu expanded, K3 with Max selected, and
kimi-for-codingwith no Thinking row. They will be attached through the GitHub web UI when the upstream PR is created. On the public base revision, Kimi agents do not render a Thinking row, so no fabricated before screenshot is provided.